不知道大家有没有运行多个异步函数并将结果传入到一个数组,最后结果却发现数组是空的经历?
就像上面的代码示例那样。那么数组为什么是空的呢?!
在运行其他代码之前一直等待异步函数完成似乎是一项不可能的任务。
这是因为你没有使用工具。
当你学会了如何将Promise.all
和.map
组合起来使用时,你会意识到这个任务比你想象的要容易。
为什么forEach里面async/await没有完成预期工作
当你需要对数组中的每个元素运行异步操作时,你可能会自然而然地使用.forEach()
方法。
例如,有一个用户ID数组,你希望获取用户并将用户名推送到数组。最后记录用户名,但遗憾的是,数组为空。
const userIds = [1, 2, 3];
const usernames = [];
userIds.forEach(async (userId) => {
const user = await fetchUserById(userId);
usernames.push(user.username);
});
// 很遗憾,这将会打印一个空的数组 🙁
console.log(usernames);
让我们试着用JavaScript运行时的动画来解释:
首先要注意的是,fetchUserById
请求是在forEach
方法中同时启动的。它们被发送到后台任务,直到完成(浏览器中为WebAPI,Node.js中为C++线程池)。
在所有请求启动后,程序继续并记录username
数组,当然此时该数组为空,因为尚未推送任何内容。
你看,像forEach
这样的JavaScript函数式方法并不涉及promise
。不但同步触发回调函数,而且无需在每次迭代之间等待promise
。
然后,一段时间后,请求完成,数组中将填充用户名。之后,程序退出,唯独遗漏用户名。
请求的完成顺序是随机的,很少与发出请求的顺序相同。这也是不在forEach内部使用async/await来填充数组的另一个原因——结果的排列将与原始数组不同。
看到运行时从末尾“向上跳”到中间是令人困惑的,因为在编程中,我们学到的第一件事就是代码从上到下执行。
那么,如何在所有异步任务完成后才运行代码呢?
Promise.all和 .map,绝配
解决方案是将数组的每个元素映射到promise,并将生成的promise数组传递给Promise.all
。必须使用Promise.all
,因为await
关键字仅适用于单个promise。
Promise.all
就像一个聚合器。给定promise数组,会返回一个在所有promise都实现后实现的单个promise。返回的promise解析为包含输入promises结果的数组。
现在来修改示例,将用户ID映射到一个promise数组,其中每个promise都使用用户名进行解析。然后把promises传递给promise.all
,等待并记录结果。
const userIds = [1, 2, 3];
// Map each userId to a promise that eventually fulfills
// with the username
const promises = userIds.map(async (userId) => {
const user = await fetchUserById(userId);
return user.username;
});
// Wait for all promises to fulfill first, then log
// the usernames
const usernames = await Promise.all(promises);
console.log(usernames);
在将promises传递给Promise.all
之前,这里之所以将promises分配给单独的变量,是为了更容易理解发生了什么。在实践中,你经常会看到直接在Promise.all
中编写的map函数。
就是这样!现在你知道如何运行多个异步任务并在执行剩余代码之前等待异步任务的完成了吧。
好的,下次你再在forEach
内部看到async/await
,就应该敲响警钟——代码可能无法按预期工作。