按顺序完成异步操作
实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。
Promise 的写法如下:
1 | function logInOrder(urls) { |
上面代码使用fetch
方法,同时远程读取一组 URL。每个fetch
操作都返回一个 Promise 对象,放入 textPromises 数组。然后,reduce
方法依次处理每个 Promise 对象,然后使用then
,将所有 Promise 对象连起来,因此就可以依次输出结果。
这种写法不太直观,可读性比较差。下面是 async 函数实现:
1 | async function logInOrder(urls) { |
上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并行发出远程请求。
1 | async function logInOrder(urls) { |
上面代码中,虽然map
方法的参数是async函数
,但它是并行执行的,因为只有async函数
内部是继发执行,外部不受影响。后面的for...of
循环内部使用了await
,因此实现了按顺序输出。
再举个例子,下面这两段代码有什么区别?
1 | import fs from 'fs-promise' |
1 | import fs from 'fs-promise' |
第一段每个forEach
的回调都是一个async
函数,所以每个回调有自己的阻塞范围,回调内的await
是相互独立的,不会互相阻塞,所以可以看为是并行的。
第二段只有一个async
函数,就是外层的printFiles
,for...of
内的所有await
不是互相独立的,要按次序执行,所以可以看成是继发的。
所以如果我们希望按顺序读取文件,那么第一段显然是错的,第二段是对的。
其他实现方法:
1 | async function printFiles () { |
仍然是并行的,forEach
是没有返回值的,而用map
配合Promise.all
,可以通过await
获得返回的 promise 数组。
下面通过reduce
实现,是按顺序执行的:
1 | async function printFiles () { |
ES2018 的异步遍历器,是按顺序执行的:
1 | async function printFiles () { |
关于理解异步遍历器,看下面的例子:
1 | let timeout = 1000 |
并行执行异步操作
如果一组异步操作是无关联相互独立的,比如首屏调用多个不相互依赖的接口,可以使用Promise.all
:
1 | (async () => { |
但如果其中任何一个接口挂掉了,任一 promise 被reject
,则直接会被catch
捕获走catch
内的逻辑,那么其他接口的返回数据就无法获取,这显然不是我们想看到的。
解决办法就是对每一个 promise 做异常处理:
1 | Promise.all([a, b, c].map(p => p.catch(e => {...}))) |
也就是在第一个catch
内并不抛出异常,而是返回给下一个then
,在下一个then
内判断哪些是正常返回,哪些是异常返回。举个例子:
1 | const a = Promise.resolve(1) |
虽然 b 被reject
,但并不影响其他resolve
的返回值。
配合await
:
1 | (async () => { |
通过 ES2020 的Promise.allSettled()
实现。和Promise.all
类似,但即使有 promise 失败变为rejected
,也不会影响其他 promise 的状态。
1 | const a = Promise.resolve(1) |
控制 promise 并发
当Promise.all
中任务数量过多时,希望控制每个时刻并发执行的promise
任务数量是固定的。当所有promise
完成时,触发总的resolve
。
因为Promise.all
中的任务,其实是promise
实例化的时候执行的,所以要限制并发,就要限制promise
的实例化。就是把生成promise
数组的的控制权交给并发控制逻辑。
这里参考一个第三方的库 async-pool 的实现。去掉不必要的代码,大概内容如下:
1 | /** |
如何使用:
1 | const array = [1000, 5000, 2000, 3000] |
大概过程如下:
- 将 1000、5000 和 2000 的
promise
依次加入ret
和executing
队列 - 之后发现达到 poolLimit = 3,调用
Promise.race(executing)
- 1000 的
promise
会率先resolve
,并从executing
队列移除,之后继续递归 - 将 3000 的
promise
加入executing
队列,此时 5000 和 2000 的promise
还是pending
状态,executing
队列中为 5000、2000、3000 三个任务,达到poolLimit
,再次调用Promise.race
, - 一秒后 2000 的
promise
被resolve
,从队列中移除,接着发现遍历结束,中断递归,最后调用Promise.all(ret)
- 此时
ret
队列中 1000 和 2000 的promise
都是resolve
,等待 3000 和 5000 的都完成后,最后触发Promise.all
实例的回调,并将结果返回
参考资料
ECMAScript 6 入门 - 阮一峰