异步任务分为两大类:并行与串行。并行任务相对简单,串行任务则有多种变体。
Concurrency
并行的异步任务本质上是这样一个问题:单个异步任务仅可能返回两种状态:正常、异常。假设有 m(m > 1)个并发执行的异步任务合成一个异步任务集合,那么这个集合(也是一个异步任务)应该返回什么?
- 全部正常才正常(只要有一个子任务异常,就返回异常)
- 全部正常时,返回所有子任务结果的集合:
Promise.all
- 全部正常时,返回最先完成的子任务的结果:
Promise.race
- 全部正常时,返回所有子任务结果的集合:
- 存在正常就正常
- 不管有没有异常,等所有子任务结束,返回所有结果的集合:
Promise.settle
- 不管有没有异常,只要出现一个子任务正常,就返回该任务的结果:
Promise.any
- 不管有没有异常,等所有子任务结束,返回所有结果的集合:
- 存在 n 个正常才正常,否则返回异常:
Promise.some
Sequence
序列化异步问题是说:有些异步任务不能同时执行(互斥关系),必须等上一个执行完,才能执行下一个。来看几个典型场景:
1、下载队列问题
比如用户可以勾选任意多个文件并下载,假设我们的策略是下载完第一个再下载第二个,这种任务应该怎么实现呢?
可以借助某种队列或循环机制,比如通过 reduce 或 for of 来实现:
写法一:
写法二:
写法三:
市面上也有一些现成的库可以处理这种问题,比如 async.series、deferred-queue、promise-sequence 等。
2、loading 问题
假设每个接口请求发起时都会展示 loading,请求结束隐藏 loading。接口请求可能有很多,但每时每刻界面上只能有一个 loading。比如 a 请求发出,展示 loading,之后 b 请求发出,如果 a 请求结束时,b 还没有结束,那么继续展示 loading,反之则隐藏 loading,这怎么实现呢?
可以考虑一种引用计数的策略:
var loading = {
count: 0,
el: document.createTextNode('loading'),
start () {
if (this.count === 0) {
document.body.appendChild(this.el)
}
this.count += 1
},
stop () {
this.count -= 1
if (this.count === 0) {
document.body.removeChild(this.el)
}
}
}
复制代码
3、竞态问题
竞态问题是指同一类请求,先后发送,以哪一个的返回为准?比如用户搜索 A 类电影,由于接口迟迟未返回,用户选择搜索 B 类电影,如果 B 的请求还没有返回,A 却返回了,这时怎么办?
每个操作都只是单个异步任务,而不是一个序列任务,但用户的多次操作却构成了一个序列任务。
显然只有最新的请求才应该被使用,我们可以用时间戳来标识每个请求。
const map = {
'fetchMovie': 0
}
function fetchMovie () {
const stamp = Date.now()
map.fetchMovie = stamp
fetch(url, params).then(res => {
if (stamp < map.fetchMovie) return null // 该请求已过时
return res
})
}
复制代码
不过这种方案侵入性比较强,能不能实现一个类似 redux-saga 中的 takeLatest 的方法呢?takeLatest 的基本思路是:只要有最新的请求,就将之前的请求 cancel 掉,但 promise 没有办法 cancel(I know bluebird),这怎么办呢?
// 模拟一个在 t 时间后返回结果的接口请求
function request (t) {
return new Promise(resolve => {
setTimeout(function () {
resolve(t)
}, t)
})
}
const map = {}
function takeLatest (key, fn) {
if (!map[key]) {
map[key] = 1
}
return function () {
let resolve
let reject
// 嘿嘿
const a = new Promise((_res, _rej) => {
resolve = _res
reject = _rej
})
const t = Date.now()
map[key] = t
fn.apply(null, arguments).then(res => {
if (t < map[key]) return
resolve(res)
}).catch(error => {
if (t < map[key]) return
reject(error)
})
return a
}
}
// 测试
const f1 = takeLatest('fetchMovie', request)
const f2 = takeLatest('fetchOther', request)
f1(3000).then(res => {
console.log(res)
})
f2(3050).then(res => {
console.log(res)
})
setTimeout(() => {
f1(1000).then(res => {
console.log(res)
})
f2(1050).then(res => {
console.log(res)
})
}, 1000)
// 返回 setTimeout 里的两个“最新”的请求结果:1000,1050
复制代码
Webpack 的异步任务管理
webpack 原本只是一个打包工具,后来逐步演化成前端构建工具,构建的核心是构建过程管理,或者说构建任务管理。任务有同步、有异步,过程有并行、有串行,看上去很复杂,但是 webpack 通过 tapable 这个库对各类同步、异步场景做了很好的抽象,tapable 提供了一系列被称为 hook 的 api 来处理各类同步、异步的场景。
{
AsyncParallelHook,// 异步并行任务
AsyncSeriesHook, // 异步序列任务
AsyncSeriesBailHook, // 可中断的异步序列任务
AsyncSeriesWaterfallHook // 可传参的异步序列任务
...
}
复制代码
这篇文章 讲得很清楚,推荐。