发布于 2026-01-06 15 阅读
0

不要使用异步/等待!哎呀!

不要使用异步/等待!哎呀!

假设我们需要对数组中的元素执行一些 I/O 操作,例如使用某些 API 根据猫的 ID 获取猫的主人。

const catIDs = [132, 345, 243, 121, 423];
Enter fullscreen mode Exit fullscreen mode

假设我们决定运用新学的 async/await 技能来完成这项工作。async/await 可以消除回调函数的需求(在大多数情况下),使异步代码看起来与同步代码类似。但如果我们忘记了我们仍然在处理异步代码,就可能会犯一些错误,从而完全违背并发的初衷。

我们可能会忍不住想这样做:

async function fetchOwners(catIDs) {
    const owners = [];
    for (const id of catIDs) {
        const cat = await fetchCat(id);
        const owner = await fetchOwner(cat.ownerID);
        owners.push(owner);
    }
    return owners;
}
Enter fullscreen mode Exit fullscreen mode

我们的 Oopsie 是什么? 🤷‍♂️

免责声明:如果您知道这里提到的“错误”是什么,那么您可能已经知道自己在做什么了。您或许也知道这种行为的某种应用场景,所以称之为“错误”可能不太公平。本文旨在帮助大家熟悉这种 async/await 行为。

我们运行了代码,一切似乎都正常。但是,我们使用 async/await 的方式存在一个明显的错误。问题在于我们await在 for 循环中使用了它。这实际上是一种常见的代码异味,即在 for 循环中直接使用`.push async/await` 来执行数组转换,而不是使用.map`await`(我们稍后会详细讨论这一点)。

由于使用await了 for 循环,这个看似同步的fetchOwners函数实际上是按顺序(某种程度上)获取猫的主人信息,而不是并行执行。代码await在进入下一次 for 循环迭代获取下一只猫的主人信息之前,会先获取当前这只猫的主人信息。获取一只猫的主人信息并不依赖于其他任何猫,但我们却表现得好像它依赖于其他猫一样。因此,我们完全错过了并行获取所有猫主人信息的机会(哎呀!🤷‍♂️)。

注意:我提到“某种程度上”是按顺序进行的,因为这些顺序的 for 循环迭代与其他过程(通过事件循环)交错进行,因为 for 循环迭代await在一个async函数内。

我们应该做什么😎

我们不应该await在 for 循环内使用 for 循环。事实上,即使代码是同步的,这个问题最好也用其他方法解决。A.map是合适的解决方案,因为我们处理的问题是数组转换,即将猫咪 ID 数组转换为猫主人数组。

.map如果代码是同步的,我们会这样做。

// catIDs -> owners
const owners = catIDs.map(id => {
    const cat = fetchCatSync(id);
    const owner = fetchOwnerSync(cat.ownerID);
    return owner;
});
Enter fullscreen mode Exit fullscreen mode

由于代码实际上是异步的,我们首先需要将猫咪 ID 数组转换为 Promise 数组(向猫主人发送 Promise),然后使用该 Promise 数组解包await以获取猫主人信息。为了简单起见,这段代码没有处理被拒绝的 Promise。

// catIDs -> ownerPromises -> owners
async function fetchOwners(catIDs) {
    const ownerPromises = catIDs.map(id => {
        return fetchCat(id)
            .then(cat => fetchOwner(cat.ownerID));
    });
    const owners = await Promise.all(ownerPromises);
    return owners;
}
Enter fullscreen mode Exit fullscreen mode

为了进一步提升我们的 async/await 技能,我们可以将一个async回调函数传递给 map 方法,并将await所有中间响应(这里是获取一只猫及其主人的 ID)都放在该回调函数中。记住,async函数返回的是一个 Promise,所以我们最终得到的仍然是一个 Promise 数组作为输出.map。这段代码与之前的代码等效,只是去掉了丑陋的 `.` 语句.then

async function fetchOwners(catIDs) {
    const ownerPromises = catIDs.map(async id => {
        const cat = await fetchCat(id);
        const owner = await fetchOwner(cat.ownerID);
        return owner;
    });
    const owners = await Promise.all(ownerPromises);
    return owners;
}
Enter fullscreen mode Exit fullscreen mode

.map到底在做什么?

.map它会按顺序对每个猫的 ID 调用回调函数(其中会发出 I/O 请求)。但由于回调函数返回的是一个 Promise(或者说是一个异步函数),因此.map它不会等待上一只猫的响应到达就立即发出下一只猫的请求。

请求是按顺序发出的,但并行传输它们的响应是乱序到达的(想象一下,一只手背在身后扔出一堆回旋镖)。

所以我们现在正按计划并行联系猫主人🙌!哎呀,搞砸了!


这是我的第一篇帖子。不仅是我在DEV上的第一篇,也是我的第一篇博客文章。希望你们喜欢。

文章来源:https://dev.to/mebble/dont-make-this-asyncawait-mistake-1eao