如何在 JS 循环中正确使用 async 与 await

阿里云最近在作活动,低至2折,有兴趣能够看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。html

asyncawait 的使用方式相对简单。 蛤当你尝试在循环中使用await时,事情就会变得复杂一些。前端

在本文中,分享一些在若是循环中使用await值得注意的问题。git

准备一个例子

对于这篇文章,假设你想从水果篮中获取水果的数量。github

const fruitBasket = {
 apple: 27,
 grape: 0,
 pear: 14
};

你想从fruitBasket得到每一个水果的数量。 要获取水果的数量,可使用getNumFruit函数。数组

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

const numApples = getNumFruit('apple');
console.log(numApples); //27

如今,假设fruitBasket是从服务器上获取,这里咱们使用 setTimeout 来模拟。promise

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

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

getNumFruit("apple").then(num => console.log(num)); // 27

最后,假设你想使用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')
}

图片描述

在 for 循环中使用 await

首先定义一个存放水果的数组:app

const fruitsToGet = [“apple”, “grape”, “pear”];

循环遍历这个数组:异步

const forLoop = async _ => {
  console.log('Start');
  
  for (let index = 0; index < fruitsToGet.length; index++) {
    // 获得每一个水果的数量
  }

  console.log('End')
}

for循环中,过上使用getNumFruit来获取每一个水果的数量,并将数量打印到控制台。async

因为getNumFruit返回一个promise,咱们使用 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')
}

当使用await时,但愿JavaScript暂停执行,直到等待 promise 返回处理结果。这意味着for循环中的await 应该按顺序执行。

结果正如你所预料的那样。

“Start”;
“Apple: 27”;
“Grape: 0”;
“Pear: 14”;
“End”;

图片描述

这种行为适用于大多数循环(好比whilefor-of循环)…

可是它不能处理须要回调的循环,如forEachmapfilterreduce。在接下来的几节中,咱们将研究await 如何影响forEach、map和filter

在 forEach 循环中使用 await

首先,使用 forEach 对数组进行遍历。

const forEach = _ => {
  console.log('start');

  fruitsToGet.forEach(fruit => {
    //...
  })

  console.log('End')
}

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

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

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

  console.log('End')
}

我指望控制台打印如下内容:

“Start”;
“27”;
“0”;
“14”;
“End”;

但实际结果是不一样的。在forEach循环中等待返回结果以前,JavaScrip先执行了 console.log('End')。

实际控制台打印以下:

‘Start’
‘End’
‘27’
‘0’
‘14’

图片描述

JavaScript 中的 forEach不支持 promise 感知,也支持 asyncawait,因此不能在 forEach 使用 await

在 map 中使用 await

若是在map中使用await, map 始终返回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')
}
      

“Start”;
“[Promise, Promise, Promise]”;
“End”;

clipboard.png

若是你在 map 中使用 awaitmap 老是返回promises,你必须等待promises 数组获得处理。 或者经过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')
}

运行结果以下:

图片描述

若是你愿意,能够在promise 中处理返回值,解析后的将是返回的值。

const mapLoop = _ => {
  // ...
  const promises = fruitsToGet.map(async fruit => {
    const numFruit = await getNumFruit(fruit);
    return numFruit + 100
  })
  // ...
}
 
“Start”;
“[127, 100, 114]”;
“End”;

在 filter 循环中使用 await

当你使用filter时,但愿筛选具备特定结果的数组。假设过滤数量大于20的数组。

若是你正常使用filter (没有 await),以下:

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

  const moreThan20 =  fruitsToGet.filter(async fruit => {
    const numFruit = await fruitBasket[fruit]
    return numFruit > 20
  })
  
  console.log(moreThan20) 
  console.log('END')
}

运行结果

Start
["apple"]
END

filter 中的await不会以相同的方式工做。 事实上,它根本不起做用。

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

  const moreThan20 =  await fruitsToGet.filter(async fruit => {
    const numFruit = fruitBasket[fruit]
    return numFruit > 20
  })
  
  console.log(moreThan20) 
  console.log('END')
}


// 打印结果
Start
["apple", "grape", "pear"]
END

clipboard.png

为何会发生这种状况?

当在filter 回调中使用await时,回调老是一个promise。因为promise 老是真的,数组中的全部项都经过filter 。在filter 使用 await类如下这段代码

const filtered = array.filter(true);

filter使用 await 正确的三个步骤

  1. 使用map返回一个promise 数组
  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')
}

图片描述

在 reduce 循环中使用 await

若是想要计算 fruitBastet中的水果总数。 一般,你可使用reduce循环遍历数组并将数字相加。

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')
}

运行结果:

clipboard.png

当你在 reduce 中使用await时,结果会变得很是混乱。

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

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

  console.log(sum)
  console.log('End')
}

图片描述

[object Promise]14 是什么 鬼??

剖析这一点颇有趣。

  1. 在第一次遍历中,sum0numFruit27(经过getNumFruit(apple)的获得的值),0 + 27 = 27
  2. 在第二次遍历中,sum是一个promise。 (为何?由于异步函数老是返回promises!)numFruit0.promise 没法正常添加到对象,所以JavaScript将其转换为[object Promise]字符串。 [object Promise] + 0object Promise] 0
  3. 在第三次遍历中,sum 也是一个promisenumFruit14. [object Promise] + 14[object Promise] 14

解开谜团!

这意味着,你能够在reduce回调中使用await,可是你必须记住先等待累加器!

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

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

  console.log(sum)
  console.log('End')
}

图片描述

可是从上图中看到的那样,await 操做都须要很长时间。 发生这种状况是由于reduceLoop须要等待每次遍历完成promisedSum

有一种方法能够加速reduce循环,若是你在等待promisedSum以前先等待getNumFruits(),那么reduceLoop只须要一秒钟便可完成:

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

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

  console.log(sum)
  console.log('End')
}

图片描述

这是由于reduce能够在等待循环的下一个迭代以前触发全部三个getNumFruit promise。然而,这个方法有点使人困惑,由于你必须注意等待的顺序。

在reduce中使用wait最简单(也是最有效)的方法是

  1. 使用map返回一个promise 数组
  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')
    }

这个版本易于阅读和理解,须要一秒钟来计算水果总数。

图片描述

从上面看出来什么

  1. 若是你想连续执行await调用,请使用for循环(或任何没有回调的循环)。
  2. 永远不要和forEach一块儿使用await,而是使用for循环(或任何没有回调的循环)。
  3. 不要在 filterreduce 中使用 await,若是须要,先用 map 进一步骤处理,而后在使用 filterreduce 进行处理。

交流

干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,便可看到福利,你懂的。

clipboard.png

相关文章
相关标签/搜索