通常前端一看到要遍历数组,就会用 forEach。
// 有一个异步方法
function sleepPromise(msg, t) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`promise-${msg} ${t}s`);
}, t * 1000);
})
}
const tasks = [
['a', 3],
['b', 1],
['c', 2],
];
tasks.forEach(async task => {
const [ msg, t ] = task;
const m = await sleepPromise(msg, t);
console.log(m);
})
// 输出结果是:
// promise-b 1s
// promise-c 2s
// promise-a 3s
这种写法并不对,其实是将遍历写成了同步。 问题处在forEach 并不支持异步。你在 forEach 方法的前面加不加 await 关键字都是无效的,因为它的内部没有处理异步的逻辑。
forEach 是es5 的api,promise 是es6 的方法,forEach以后也不会支持异步的。
试试用串联写法
// 串行写法
for (const task of tasks) {
const [ msg, t ] = task;
const m = await sleepPromise(msg, t);
console.log(m);
}
// 输出的结果
// promise-a 3s
// promise-b 1s
// promise-c 2s
使用普通的 for 循环写法,await 的外层函数就仍就是 loopAysnc 方法,就能正确保存阻塞代码。
可以处理异步,但是串联写法会慢很多,耗时久。如果我们的这些请求是有顺序的依赖关系的,这样写是没问题。
如果没有顺序依赖,这样写就不合理,那我们需要写一个并行的异步。并且还要保证所有异步都执行完后才执行下一步。我们可以用 Promise.all()
。
// 并行写法
const taskPromises = tasks.map(task => {
const [ msg, t ] = task;
return sleepPromise(msg, t).then(m => {
console.log(m);
});
});
await Promise.all(taskPromises);
首先,我们需要根据 tasks 数组生成对应的 promise 对象数组,然后传入到 Promise.all 方法中执行。
这样,这些异步方法就会同时执行。当所有异步都执行完毕后,代码才往下执行。
速度很快,很强大。
回到forEach , 前面说到 forEach 底层并没有实现异步的处理,才导致阻塞失效,那么我们其实不妨实现支持异步的简易 forEach。
async function forEach(arr, fn) {
const fns = [];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
fns.push(fn(item, i, arr));
}
await Promise.all(fns);
}
串行实现:
async function forEach(arr, fn) {
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
await fn(item, i, arr);
}
}
用法
await forEach(tasks, async task => {
const [ msg, t ] = task;
const m = await sleepPromise(msg, t);
console.log(m);
})
总结:
一般来说,我们更常用 Promise.all 的并行执行异步的方法,常见于数据库查找一些 id 对应的数据的场景。
for 循环的串行写法适用于多个异步有依赖的情况,比如找最终推荐人。
forEach 则是纯粹的错误写法,除非是不需要使用 async/await 的情况。