异步串行的4种实现及细微区别

2,237 阅读4分钟

事情起源于去年的某一天

需要做一个批量上传的功能,而接口只允许上传单个,一下子调用多个接口服务器又撑不住 ,所以只能用串行的方案,一次只调一个接口,拿到返回值后再调下一个

于是问题就抽象成了:多个异步方法如何串行调用


你可知茴字有几种写法?

先模拟一个请求

let request = param => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`request: ${param}`)
      resolve(param)
    }, 1000)
  })
}

待传参数数组,数组长度即为待调的接口次数

const params = ['a', 'b', 'c']
  • 方法一:async/await

async/await是最先想到的方法

这也是个人认为最简单,最简洁,最优雅的一种写法

for (const v of params) {
  await request(v)
}
  • 方法二: 循环

for循环配合promise,只比方法一多了一个变量

let p = Promise.resolve()
for (const v of params) {
  p = p.then(() => request(i))
}
  • 方法三:递归

既然for可以,那递归也就可以

但递归和前两者相比就显得有点麻烦了

function loop(params) {
  request(params.splice(0, 1)).then(() => {
    if (params.length) {
      loop(params)
    } else return
  })
}

loop(params)
  • 方法四:reduce

没想到吧.jpg

reduce配合promise竟然会有这种效果

其实仔细看它其实和方法二有点类似

就是把方法二里的变量变成了acc

params.reduce((acc, param) => {
  return acc.then(() => request(param)) 
}, Promise.resolve())

但是

这四种方法仅仅是只有以上这些表面上的异同吗

真正运行起来怎么样呢

我不知道

但浏览器可以让我知道


不多不多!多乎哉?不多也

新建一个test.html,加上上文的模拟请求函数

把定时器的时间缩短为100ms

函数运行过程尽量短一点,避免垃圾回收影响结果

加上模拟请求参数,5个

const params = Array.from({length: 5}).map((item, index) => index)

加一个按钮,绑定点击事件,点击时,调用上文四个方法的其中一个

打开控制台-Performance,统一记录十秒,第三秒点击按钮

把上面四种方法分别测一遍后,得到了四份结果

async/await

async/await

循环

循环

递归

递归

reduce

reduce

中间Main里第一个较粗的黄色柱子是点击事件

后面5个细柱子是执行的5次函数

下面蓝色线是内存占用情况,每次上涨都对应了函数的执行

这四张图里有一张和其他图都不一样

你发现了吗


一个区别

没错,就是第二张:循环!

在点击的那个时刻,它的内存就上涨了

而其他图的内存都是在第一个定时器结束时,才开始上涨的

为什么偏偏是循环最特殊呢?

可以把整个执行过程看做一条promise链

promise.then().then().then()....

只有循环是在一开始就把这条链构建出来了

而其他方法都是只有在前一个then执行结束后才会再添一个then

即循环可以看做是首次就完全加载

其他三个方法可以看成是按需加载

另一个区别

放大Main里点击事件的区域

async/await

async/await

循环

循环

递归

循环

reduce

reduce

根据点击时request的执行阶段可以分为两类

循环reduce:在Microtasks即微任务下

async/await递归:在点击事件回调即正常的主任务下

为什么会出现这种不同呢?还记得上文说的reduce循环有点类似吗,再结合之前promise链的说法,不难看出

他们的起点是Promise.resolve(),第一个request是在then里的调用的

而另外两个的起点是request(),第一个request是作为普通函数调用的

异步中的方法都会在Microtasks中执行,Microtasks又是紧跟在主任务后的,所以才会出现这种执行队列的不同

这就又牵扯到浏览器的事件队列机制了,记得掘金有文章专门分析这个机制,这里就不专门说了

总结

异步串行平时几乎不会用到,但里面要挖还是可以挖出很多东西来的

这四种方法首推async/await

循环的话,如果有很长的then链要小心一点,一下子构造出太长的链可能不大好

递归主要是写法太烦,会多出一个函数

reduce可读性会欠缺一点,炫技的话还是不错的


感谢阅读。