事情起源于去年的某一天
需要做一个批量上传的功能,而接口只允许上传单个,一下子调用多个接口服务器又撑不住 ,所以只能用串行的方案,一次只调一个接口,拿到返回值后再调下一个
于是问题就抽象成了:多个异步方法如何串行调用
你可知茴字有几种写法?
先模拟一个请求
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
循环
递归
reduce
中间Main里第一个较粗的黄色柱子是点击事件
后面5个细柱子是执行的5次函数
下面蓝色线是内存占用情况,每次上涨都对应了函数的执行
这四张图里有一张和其他图都不一样
你发现了吗
一个区别
没错,就是第二张:循环!
在点击的那个时刻,它的内存就上涨了
而其他图的内存都是在第一个定时器结束时,才开始上涨的
为什么偏偏是循环最特殊呢?
可以把整个执行过程看做一条promise链
promise.then().then().then()....
只有循环是在一开始就把这条链构建出来了
而其他方法都是只有在前一个then执行结束后才会再添一个then
即循环可以看做是首次就完全加载
其他三个方法可以看成是按需加载
另一个区别
放大Main里点击事件的区域
async/await
循环
递归
reduce
根据点击时request的执行阶段可以分为两类
循环、reduce:在Microtasks即微任务下
async/await、递归:在点击事件回调即正常的主任务下
为什么会出现这种不同呢?还记得上文说的reduce和循环有点类似吗,再结合之前promise链的说法,不难看出
他们的起点是Promise.resolve(),第一个request是在then里的调用的
而另外两个的起点是request(),第一个request是作为普通函数调用的
异步中的方法都会在Microtasks中执行,Microtasks又是紧跟在主任务后的,所以才会出现这种执行队列的不同
这就又牵扯到浏览器的事件队列机制了,记得掘金有文章专门分析这个机制,这里就不专门说了
总结
异步串行平时几乎不会用到,但里面要挖还是可以挖出很多东西来的
这四种方法首推async/await
循环的话,如果有很长的then链要小心一点,一下子构造出太长的链可能不大好
递归主要是写法太烦,会多出一个函数
reduce可读性会欠缺一点,炫技的话还是不错的
感谢阅读。