问题:对于异步代码,forEach 并不能保证按顺序执行。

举个例子:

  1. async function test() {
  2. let arr = [4, 2, 1]
  3. arr.forEach(async item => {
  4. const res = await handle(item)
  5. console.log(res)
  6. })
  7. console.log('结束')
  8. }
  9. function handle(x) {
  10. return new Promise((resolve, reject) => {
  11. setTimeout(() => {
  12. resolve(x)
  13. }, 1000 * x)
  14. })
  15. }
  16. test()

我们期望的结果是:

  1. 4
  2. 2
  3. 1
  4. 结束

但是实际上会输出:

  1. 结束
  2. 1
  3. 2
  4. 4

问题原因

这是为什么呢?我想我们有必要看看forEach底层怎么实现的。

  1. // 核心逻辑
  2. for (var i = 0; i < length; i++) {
  3. if (i in array) {
  4. var element = array[i];
  5. callback(element, i, array);
  6. }
  7. }

可以看到,forEach 拿过来直接执行了,这就导致它无法保证异步任务的执行顺序。比如后面的任务用时短,那么就又可能抢在前面的任务之前执行。

解决方案

如何来解决这个问题呢?

其实也很简单, 我们利用for...of就能轻松解决。

  1. async function test() {
  2. let arr = [4, 2, 1]
  3. for(const item of arr) {
  4. const res = await handle(item)
  5. console.log(res)
  6. }
  7. console.log('结束')
  8. }

解决原理——Iterator

好了,这个问题看起来好像很简单就能搞定,你有想过这么做为什么可以成功吗?

其实,for…of并不像forEach那么简单粗暴的方式去遍历执行,而是采用一种特别的手段——迭代器去遍历。

首先,对于数组来讲,它是一种可迭代数据类型。那什么是可迭代数据类型呢?

原生具有[Symbol.iterator]属性数据类型为可迭代数据类型。如数组、类数组(如arguments、NodeList)、Set和Map。

可迭代对象可以通过迭代器进行遍历。

  1. let arr = [4, 2, 1];
  2. // 这就是迭代器
  3. let iterator = arr[Symbol.iterator]();
  4. console.log(iterator.next());
  5. console.log(iterator.next());
  6. console.log(iterator.next());
  7. console.log(iterator.next());
  8. // {value: 4, done: false}
  9. // {value: 2, done: false}
  10. // {value: 1, done: false}
  11. // {value: undefined, done: true}

因此,我们的代码可以这样来组织:

  1. async function test() {
  2. let arr = [4, 2, 1]
  3. let iterator = arr[Symbol.iterator]();
  4. let res = iterator.next();
  5. while(!res.done) {
  6. let value = res.value;
  7. console.log(value);
  8. await handle(value);
  9. res = iterater.next();
  10. }
  11. console.log('结束')
  12. }
  13. // 4
  14. // 2
  15. // 1
  16. // 结束

多个任务成功地按顺序执行!其实刚刚的for…of循环代码就是这段代码的语法糖。

重新认识生成器

回头再看看用iterator遍历[4,2,1]这个数组的代码。

  1. let arr = [4, 2, 1];
  2. // 迭代器
  3. let iterator = arr[Symbol.iterator]();
  4. console.log(iterator.next());
  5. console.log(iterator.next());
  6. console.log(iterator.next());
  7. console.log(iterator.next());
  8. // {value: 4, done: false}
  9. // {value: 2, done: false}
  10. // {value: 1, done: false}
  11. // {value: undefined, done: true}

咦?返回值有valuedone属性,生成器也可以调用 next,返回的也是这样的数据结构,这么巧?!

没错,生成器本身就是一个迭代器

既然属于迭代器,那它就可以用for…of遍历了吧?

当然没错,不信来写一个简单的斐波那契数列(50以内):

  1. function* fibonacci(){
  2. let [prev, cur] = [0, 1];
  3. console.log(cur);
  4. while(true) {
  5. [prev, cur] = [cur, prev + cur];
  6. yield cur;
  7. }
  8. }
  9. for(let item of fibonacci()) {
  10. if(item > 50) break;
  11. console.log(item);
  12. }
  13. // 1
  14. // 1
  15. // 2
  16. // 3
  17. // 5
  18. // 8
  19. // 13
  20. // 21
  21. // 34

是不是非常酷炫?这就是迭代器的魅力:)同时又对生成器有了更深入的理解,没想到我们的老熟人Generator还有这样的身份。

以上便是本文的全部内容,希望对你有所启发。