JavaScript 循环中的 async 和 await

2025-06-10

JavaScript 循环中的 async 和 await

基本async和简单。当你尝试使用循环时,await事情会变得有点复杂。await

await在本文中,我想分享一些在循环中使用时需要注意的陷阱。

开始之前

我假设你知道如何使用asyncawait。如果你不知道,请先阅读上一篇文章,熟悉后再继续。

准备一个例子

对于本文,假设您想获取水果篮中的水果数量。

const fruitBasket = {
  apple: 27,
  grape: 0,
  pear: 14
};
Enter fullscreen mode Exit fullscreen mode

你想从水果篮中获取每种水果的数量。要获取水果的数量,可以使用getNumFruit函数。

const getNumFruit = fruit => {
  return fruitBasket[fruit];
};

const numApples = getNumFruit("apple");
console.log(numApples); // 27
Enter fullscreen mode Exit fullscreen mode

现在,假设fruitBasket远程服务器上有一个 live 对象。访问该对象需要一秒钟。我们可以用超时来模拟这一秒的延迟。(如果您对超时代码理解有困难,请参阅上一篇文章)。

const sleep = ms => {
  return new Promise(resolve => setTimeout(resolve, ms));
};

const getNumFruit = fruit => {
  return sleep(1000).then(v => fruitBasket[fruit]);
};

getNumFruit("apple").then(num => console.log(num)); // 27
Enter fullscreen mode Exit fullscreen mode

最后,假设您想使用awaitgetNumFruit在异步函数中获取每种水果的数量。

const control = async _ => {
  console.log("Start");

  const numApples = await getNumFruit("apple");
  console.log(numApples);

  const numGrapes = await getNumFruit("grape");
  console.log(numGrapes);

  const numPears = await getNumFruit("pear");
  console.log(numPears);

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

控制台显示“Start”。一秒后,输出 27。再过一秒,输出 0。再过一秒,输出 14,然后显示“End”。

有了这个,我们就可以开始await循环查看了。

在 for 循环中等待

假设我们有一组想要从水果篮中取出的水果。

const fruitsToGet = ["apple", "grape", "pear"];
Enter fullscreen mode Exit fullscreen mode

我们将循环遍历这个数组。

const forLoop = async _ => {
  console.log("Start");

  for (let index = 0; index < fruitsToGet.length; index++) {
    // Get num of each fruit
  }

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

在 for 循环中,我们将使用它getNumFruit来获取每个水果的数量。我们还会将该数量记录到控制台中。

由于getNumFruit返回一个承诺,我们可以await在记录它之前解析值。

const forLoop = async _ => {
  console.log("Start");

  for (let index = 0; index < fruitsToGet.length; index++) {
    const fruit = fruitsToGet[index];
    const numFruit = await getNumFruit(fruit);
    console.log(numFruit);
  }

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

当你使用 时await,你期望 JavaScript 暂停执行,直到等待的 Promise 得到解决。这意味着awaitfor 循环中的 s 应该被串行执行。

结果正如您所期望的。

"Start";
"Apple: 27";
"Grape: 0";
"Pear: 14";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台显示“Start”。一秒后,输出 27。再过一秒,输出 0。再过一秒,输出 14,然后显示“End”。

此行为适用于大多数循环(如whilefor-of循环)...

但它不适用于需要回调的循环。需要回退的循环示例包括forEach、、map。我们将在接下来的几节中探讨如何影响和。filterreduceawaitforEachmapfilter

forEach 循环中的 Await

我们将执行与 for 循环示例中相同的操作。首先,让我们循环遍历水果数组。

const forEachLoop = _ => {
  console.log("Start");

  fruitsToGet.forEach(fruit => {
    // Send a promise for each fruit
  });

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

接下来,我们将尝试使用 来获取水果的数量getNumFruit。(注意async回调函数中的关键字。我们需要这个async关键字,因为await位于回调函数中)。

const forEachLoop = _ => {
  console.log("Start");

  fruitsToGet.forEach(async fruit => {
    const numFruit = await getNumFruit(fruit);
    console.log(numFruit);
  });

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

您可能希望控制台看起来像这样:

"Start";
"27";
"0";
"14";
"End";
Enter fullscreen mode Exit fullscreen mode

但实际结果却不同。JavaScriptconsole.log('End')在 forEach 循环中的 promise 得到解决之前就继续调用。

控制台按以下顺序记录:

'Start'
'End'
'27'
'0'
'14'
Enter fullscreen mode Exit fullscreen mode

控制台立即打印“Start”和“End”。一秒钟后,打印出 27、0 和 14。

JavaScript 之所以这样做,是因为forEach不支持 Promise。它不支持asyncawait。你不能await在 中使用forEach

等待地图

await如果在 a 中使用mapmap将始终返回一个 Promise 数组。这是因为异步函数始终返回 Promise。

const mapLoop = async _ => {
  console.log("Start");

  const numFruits = await fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit);
    return numFruit;
  });

  console.log(numFruits);

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode
"Start";
"[Promise, Promise, Promise]";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台立即记录“开始”、“[Promise, Promise, Promise]”和“结束”

由于map总是返回 Promise(如果你使用await),你必须等待 Promise 数组得到解决。你可以使用 来做到这一点await Promise.all(arrayOfPromises)

const mapLoop = async _ => {
  console.log("Start");

  const promises = fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit);
    return numFruit;
  });

  const numFruits = await Promise.all(promises);
  console.log(numFruits);

  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

您将获得以下内容:

"Start";
"[27, 0, 14]";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台输出“Start”。一秒钟后,它输出“[27, 0, 14]”和“End”。

如果您愿意,您可以操纵 Promise 中返回的值。解析后的值将是您返回的值。

const mapLoop = async _ => {
  // ...
  const promises = fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit);
    // Adds onn fruits before returning
    return numFruit + 100;
  });
  // ...
};
Enter fullscreen mode Exit fullscreen mode
"Start";
"[127, 100, 114]";
"End";
Enter fullscreen mode Exit fullscreen mode

使用过滤器等待

当你使用 时filter,你希望用特定的结果来过滤一个数组。假设你想创建一个包含超过 20 个水果的数组。

如果你filter正常使用(不带 await),你会像这样使用它:

// Filter if there's no await
const filterLoop = _ => {
  console.log('Start')

  const moreThan20 = await fruitsToGet.filter(fruit => {
    const numFruit = fruitBasket[fruit]
    return numFruit > 20
  })

  console.log(moreThan20)
  console.log('End')
}
Enter fullscreen mode Exit fullscreen mode

您可能认为moreThan20其中只包含苹果,因为有 27 个苹果,但有 0 个葡萄和 14 个梨。

"Start"["apple"];
("End");
Enter fullscreen mode Exit fullscreen mode

awaitin 的filter工作方式不一样。事实上,它根本不起作用。你得到的是未过滤的数组……

const filterLoop = _ => {
  console.log('Start')

  const moreThan20 = await fruitsToGet.filter(async fruit => {
    const numFruit = getNumFruit(fruit)
    return numFruit > 20
  })

  console.log(moreThan20)
  console.log('End')
}
Enter fullscreen mode Exit fullscreen mode
"Start"[("apple", "grape", "pear")];
("End");
Enter fullscreen mode Exit fullscreen mode

控制台立即记录“开始”、“['apple', 'grape', 'pear']”和“结束”

这就是发生这种情况的原因。

当你在回调await中使用时filter,回调始终是一个 Promise。由于 Promise 始终为真,因此数组中的所有内容都会通过过滤器。await在回调中使用 Promisefilter就像编写以下代码:

// Everything passes the filter...
const filtered = array.filter(true);
Enter fullscreen mode Exit fullscreen mode

await正确使用有三个步骤filter

  1. 用于map返回数组承诺
  2. await一系列的承诺
  3. filter解析值
const filterLoop = async _ => {
  console.log("Start");

  const promises = await fruitsToGet.map(fruit => getNumFruit(fruit));
  const numFruits = await Promise.all(promises);

  const moreThan20 = fruitsToGet.filter((fruit, index) => {
    const numFruit = numFruits[index];
    return numFruit > 20;
  });

  console.log(moreThan20);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode
Start["apple"];
End;
Enter fullscreen mode Exit fullscreen mode

控制台显示“Start”。一秒钟后,控制台输出“['apple']”和“End”

使用 reduce 等待

假设你想找出水果盒(fruitBastet)里水果的总数。通常情况下,你可以使用reduce循环遍历数组并计算总数。

// Reduce if there's no await
const reduceLoop = _ => {
  console.log("Start");

  const sum = fruitsToGet.reduce((sum, fruit) => {
    const numFruit = fruitBasket[fruit];
    return sum + numFruit;
  }, 0);

  console.log(sum);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

您将获得总共 41 个水果。(27 + 0 + 14 = 41)。

"Start";
"41";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台立即记录“开始”、“41”和“结束”

当您使用awaitreduce 时,结果会变得非常混乱。

// Reduce if we await getNumFruit
const reduceLoop = async _ => {
  console.log("Start");

  const sum = await fruitsToGet.reduce(async (sum, fruit) => {
    const numFruit = await getNumFruit(fruit);
    return sum + numFruit;
  }, 0);

  console.log(sum);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode
"Start";
"[object Promise]14";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台输出“Start”。一秒钟后,它输出“[object Promise]14”和“End”

什么?![object Promise]14?!

剖析这一点很有趣。

  • 在第一次迭代中,sum0numFruit是 27(从 解析出的值getNumFruit('apple'))。0 + 27是 27。
  • 在第二次迭代中,sum是一个 Promise。(为什么?因为异步函数总是返回 Promise!)为0。PromisenumFruit通常无法添加到对象,因此 JavaScript 会将其转换为[object Promise]字符串。[object Promise] + 0[object Promise]0
  • 在第三次迭代中,sum也是一个承诺。numFruit14[object Promise] + 14[object Promise]14

谜团解开了!

这意味着,您可以在回调await中使用reduce,但您必须await先记住累加器!

const reduceLoop = async _ => {
  console.log("Start");

  const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
    const sum = await promisedSum;
    const numFruit = await getNumFruit(fruit);
    return sum + numFruit;
  }, 0);

  console.log(sum);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode
"Start";
"41";
"End";
Enter fullscreen mode Exit fullscreen mode

控制台输出“Start”。三秒后,输出“41”和“End”

但是……正如你从动图上看到的,所有操作都耗时很长await。这是因为每次迭代都reduceLoop需要等待完成。promisedSum

有一种方法可以加速 reduce 循环。(感谢Tim Oxley我发现了这个)。如果你await getNumFruits()先执行await promisedSum,则reduceLoop只需一秒钟即可完成:

const reduceLoop = async _ => {
  console.log("Start");

  const sum = await fruitsToGet.reduce(async (promisedSum, fruit) => {
    // Heavy-lifting comes first.
    // This triggers all three `getNumFruit` promises before waiting for the next interation of the loop.
    const numFruit = await getNumFruit(fruit);
    const sum = await promisedSum;
    return sum + numFruit;
  }, 0);

  console.log(sum);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

控制台输出“Start”。一秒钟后,它输出“41”和“End”

这种方法之所以有效,是因为可以在等待循环的下一次迭代之前reduce触发所有三个Promise。然而,这种方法有点令人困惑,因为你必须小心操作的顺序getNumFruitawait

在 reduce 中使用最简单(也是最有效)的方法await如下:

  1. 用于map返回数组承诺
  2. await一系列的承诺
  3. reduce解析值
const reduceLoop = async _ => {
  console.log("Start");

  const promises = fruitsToGet.map(getNumFruit);
  const numFruits = await Promise.all(promises);
  const sum = numFruits.reduce((sum, fruit) => sum + fruit);

  console.log(sum);
  console.log("End");
};
Enter fullscreen mode Exit fullscreen mode

此版本简单易读,只需一秒钟即可计算出水果总数。

控制台输出“Start”。一秒钟后,它输出“41”和“End”

关键要点

  1. 如果您想await连续执行调用,请使用 for 循环(或任何没有回调的循环)。
  2. 永远不要使用awaitwith forEach。请改用 for 循环(或任何没有回调的循环)。
  3. 不要await在里面filterreduce。始终使用,然后或相应地await包含一系列承诺mapfilterreduce

感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。

鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/javascript-async-and-await-in-loops-2g43
PREV
在 JavaScript 中循环遍历对象
NEXT
如何将包发布到 npm(行业惯例)