解决await在forEach中不起作用的问题


Posted in Javascript onFebruary 25, 2021

一、前言

前两天在项目中用for遍历的时候遇到了一个坑,花了一天的时间解决。这里就记一下。

二、问题

首先引一个很简单题目:给一个数组,每隔1s打印出来.这里我把我一开始在项目中的代码贴出来.(当然这里完全和业务无关的)

const _ = require('lodash');
const echo = async (i) => {
  setTimeout(() => {
    console.log('i===>', i);
  }, 5000);
}
let arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const task = async () => {
  _.forEach(arrs, async (i) => {
    await echo(i);
  })
}
const run = async () => {
  console.log('run-start====>date:', new Date().toLocaleDateString())
  await task() ;
  console.log('run-end====>date:', new Date().toLocaleDateString())
}
(async () => {
  console.log('start...')
  await run();
  console.log('end...')
})()
// start...
// run-start====>date: 2018-8-25
// run-end====>date: 2018-8-25
// end...
// i===> 1
// i===> 2
// i===> 3
// i===> 4
// i===> 5
// i===> 6
// i===> 7
// i===> 8
// i===> 9

上面的代码和输出已经给出了,很奇怪,这里的await并没有其效果.一开始因为是加了业务,是我的业务代码出了问题,然后我就把代码抽出来了,还是不起作用,当时我是真的对对await怀疑了。

最后还是给出问题的答案:

lodash的forEach和[].forEach不支持await,如果非要一边遍历一边执行await,可使用for-of

这里给出正确的代码:

const _ = require('lodash');
const echo = async (i) => {
  return new Promise((resolve,reject)=>{
    setTimeout(() => {
      console.log('i===>', i,new Date().toLocaleTimeString());
      resolve(i) ;
    }, 2000);
  })
}
let arrs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const task = async () => {
  // _.forEach(arrs, async (i) => {
  //  await echo(ji) ;
  // })
  // arrs.forEach(async (i )=> {
  //   await echo(i) ;
  // });
  for (const i of arrs) {
    await echo(i) ;
  }
}
const run = async () => {
  console.log('run-start====>date:', new Date().toLocaleDateString())
  await task() ;
  console.log('run-end====>date:', new Date().toLocaleDateString())
}
(async () => {
  console.log('start...')
  await run();
  console.log('end...')
})()
// 输出
start...
run-start====>date: 2018-8-26
i===> 1 20:51:29
i===> 2 20:51:31
i===> 3 20:51:33
i===> 4 20:51:35
i===> 5 20:51:37
i===> 6 20:51:39
i===> 7 20:51:42
i===> 8 20:51:44
i===> 9 20:51:46
i===> 10 20:51:48
run-end====>date: 2018-8-26
end...

三、总结

当解决问题的时候,有时候可以使用排除法,比方说在这个例子中,我们知道await这个机制肯定是没问题的,如果真的有问题肯定不会轮到我测出来,那么其实剩下来的问题只能是for遍历的原因了.

因为我一开始是用lodash实现的,那么就可以想是不是lodash的forEach没有作(或者做了多余)await处理,此时就可以换种方式试试了,总的来说还是经验的问题吧。

补充:在 forEach 中使用 async/await 遇到的问题

一、问题描述

前几天,项目中遇到一个 JavaScript 异步问题:

有一组数据,需要对每一个数据进行一个异步处理,并且希望处理的时候是同步的。

用代码描述如下:

// 生成数据
const getNumbers = () => {
 return Promise.resolve([1, 2, 3])
}
// 异步处理
const doMulti = num => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if (num) {
    resolve(num * num)
   } else {
    reject(new Error('num not specified'))
   }
  }, 2000)
 })
}
// 主函数
const main = async () => {
 console.log('start');
 const nums = [1, 2, 3];
 nums.forEach(async (x) => {
  const res = await doMulti(x);
  console.log(res);
 });
 console.log('end');
};
// 执行
main();

在这个例子中,通过 forEach 遍历地将每一个数字都执行 doMulti 操作。代码执行的结果是:首先会立即打印 start、end 。2 秒后,一次性输出 1,4,9。

这个结果和我们的预期有些区别,我们是希望每间隔 2 秒,执行一次异步处理,依次输出 1,4,9。所以当前代码应该是并行执行了,而我们期望的应该是串行执行。

我们尝试把 forEach 循环替换成 for 循环:

const main = async () => {
 console.log('start');
 const nums = await getNumbers();
 for (const x of nums) {
  const res = await doMulti(x);
  console.log(res);
 }
 console.log('end');
};

执行结果完全符合了预期:依次输出:start、1, 4, 9, end 。

二、问题分析

思路都是一样的,只是使用的遍历方式不一样而已,为什么会出现这样的情况呢?在 MDN 上查找了一下 forEach 的 polyfill 参考 MDN-Array.prototype.forEach() :

// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
 Array.prototype.forEach = function(callback, thisArg) {
  var T, k;
  if (this == null) {
   throw new TypeError(' this is null or not defined');
  }
  // 1. Let O be the result of calling toObject() passing the
  // |this| value as the argument.
  var O = Object(this);
  // 2. Let lenValue be the result of calling the Get() internal
  // method of O with the argument "length".
  // 3. Let len be toUint32(lenValue).
  var len = O.length >>> 0;
  // 4. If isCallable(callback) is false, throw a TypeError exception. 
  // See: http://es5.github.com/#x9.11
  if (typeof callback !== "function") {
   throw new TypeError(callback + ' is not a function');
  }
  // 5. If thisArg was supplied, let T be thisArg; else let
  // T be undefined.
  if (arguments.length > 1) {
   T = thisArg;
  }
  // 6. Let k be 0
  k = 0;
  // 7. Repeat, while k < len
  while (k < len) {
   var kValue;
   // a. Let Pk be ToString(k).
   //  This is implicit for LHS operands of the in operator
   // b. Let kPresent be the result of calling the HasProperty
   //  internal method of O with argument Pk.
   //  This step can be combined with c
   // c. If kPresent is true, then
   if (k in O) {
    // i. Let kValue be the result of calling the Get internal
    // method of O with argument Pk.
    kValue = O[k];
    // ii. Call the Call internal method of callback with T as
    // the this value and argument list containing kValue, k, and O.
    callback.call(T, kValue, k, O);
   }
   // d. Increase k by 1.
   k++;
  }
  // 8. return undefined
 };
}

从上面的 polyfill 中的 setp 7 ,我们可以简单地理解成下面的步骤:

Array.prototype.forEach = function (callback) {
 // this represents our array
 for (let index = 0; index < this.length; index++) {
  // We call the callback for each entry
  callback(this[index], index, this);
 };
};

相当于 for 循环执行了这个异步函数,所以是并行执行,导致了一次性全部输出结果:1,4,9 。

const main = async () => {
 console.log('start');
 const nums = await getNumbers();
 // nums.forEach(async (x) => {
 //  const res = await doMulti(x);
 //  console.log(res);
 // });
 for (let index = 0; index < nums.length; index++) {
  (async x => {
   const res = await doMulti(x)
   console.log(res)
  })(nums[index])
 }
 console.log('end');
};

三、解决方案

现在,我们把问题分析清楚了。前面用 for-of 循环来代替 forEach 作为解决方案 ,其实我们也可以改造一下 forEach :

const asyncForEach = async (array, callback) => {
 for (let index = 0; index < array.length; index++) {
  await callback(array[index], index, array);
 }
}
const main = async () => {
 console.log('start');
 const nums = await getNumbers();
 await asyncForEach(nums, async x => {
  const res = await doMulti(x)
  console.log(res)
 })
 console.log('end');
};
main();

四、Eslint 问题

这时候 Eslint 又报了错:no-await-in-loop 。关于这一点,Eslint 官方文档 https://eslint.org/docs/rules/no-await-in-loop 也做了说明。

好的写法:

async function foo(things) {
 const results = [];
 for (const thing of things) {
  // Good: all asynchronous operations are immediately started.
  results.push(bar(thing));
 }
 // Now that all the asynchronous operations are running, here we wait until they all complete.
 return baz(await Promise.all(results));
}

不好的写法:

async function foo(things) {
 const results = [];
 for (const thing of things) {
  // Bad: each loop iteration is delayed until the entire asynchronous operation completes
  results.push(await bar(thing));
 }
 return baz(results);
}

其实上面两种写法没有什么好坏之分,这两种写法的结果是完全不一样的。Eslint 推荐的 “好的写法” 在执行异步操作的时候没有顺序的,“不好的写法” 中有顺序,具体需要用哪种写法还是要根据业务需求来决定。

所以,在文档的 When Not To Use It 中,Eslint 也提到,如果需要有顺序地执行,我们是可以禁止掉该规则的:

In many cases the iterations of a loop are not actually independent of each-other. For example, the output of one iteration might be used as the input to another. Or, loops may be used to retry asynchronous operations that were unsuccessful. Or, loops may be used to prevent your code from sending an excessive amount of requests in parallel. In such cases it makes sense to use await within a loop and it is recommended to disable the rule via a standard ESLint disable comment.

以上为个人经验,希望能给大家一个参考,也希望大家多多支持三水点靠木。如有错误或未考虑完全的地方,望不吝赐教。

Javascript 相关文章推荐
直接生成打开窗口代码,不必下载
May 14 Javascript
javascript 特性检测并非浏览器检测
Jan 15 Javascript
关于JavaScript的面向对象和继承有利新手学习
Jan 11 Javascript
JS格式化数字金额用逗号隔开保留两位小数
Oct 18 Javascript
JS实现的验证身份证及获取地区功能示例
Jan 16 Javascript
WebPack基础知识详解
Jan 16 Javascript
基于JavaScript实现一个简单的Vue
Sep 26 Javascript
详解vuex中action何时完成以及如何正确调用dispatch的思考
Jan 21 Javascript
Vue组件之高德地图地址选择功能的实例代码
Jun 21 Javascript
浅谈layui分页控件field参数接收对象的问题
Sep 20 Javascript
vue fetch中的.then()的正确使用方法
Apr 17 Javascript
JavaScript 面向对象程序设计详解【类的创建、实例对象、构造函数、原型等】
May 12 Javascript
JS实现百度搜索框
Feb 25 #Javascript
基于JavaScript实现随机点名器
Feb 25 #Javascript
JavaScript仿京东轮播图效果
Feb 25 #Javascript
Vue基本指令实例图文讲解
Feb 25 #Vue.js
使用webpack和rollup打包组件库的方法
Feb 25 #Javascript
vue常用高阶函数及综合实例
Feb 25 #Vue.js
原生JS实现音乐播放器的示例代码
Feb 25 #Javascript
You might like
新浪微博API开发简介之用户授权(PHP基础篇)
2011/09/25 PHP
简单了解PHP编程中数组的指针的使用
2015/11/30 PHP
javascript 浏览器判断 绑定事件 arguments 转换数组 数组遍历
2009/07/06 Javascript
Javascript this指针
2009/07/30 Javascript
js修改table中Td的值(定义td的单击事件)
2013/01/10 Javascript
关于Jquery操作Cookie取值错误的解决方法
2013/08/26 Javascript
Js参数值中含有单引号或双引号问题的解决方法
2013/11/06 Javascript
javascript基本类型详解
2014/11/28 Javascript
jQuery中:first选择器用法实例
2014/12/30 Javascript
js与jquery回车提交的方法
2015/02/03 Javascript
移动端横屏的JS代码(beta)
2016/05/16 Javascript
JavaScript的ExtJS框架中数面板TreePanel的使用实例解析
2016/05/21 Javascript
基于vue2.0+vuex+localStorage开发的本地记事本示例
2017/02/28 Javascript
关于node-bindings无法在Electron中使用的解决办法
2018/12/18 Javascript
小程序日历控件使用方法详解
2018/12/29 Javascript
javascript刷新父页面方法汇总详解
2019/10/10 Javascript
浅谈vuex为什么不建议在action中修改state
2020/02/02 Javascript
使用vue实现通过变量动态拼接url
2020/07/22 Javascript
[09:34]2018DOTA2国际邀请赛寻真——永不放弃的iG
2018/08/14 DOTA
对于Python编程中一些重用与缩减的建议
2015/04/14 Python
Python实现的微信公众号群发图片与文本消息功能实例详解
2017/06/30 Python
python机器人行走步数问题的解决
2018/01/29 Python
Django使用Channels实现WebSocket的方法
2019/07/28 Python
使用Python快乐学数学Github万星神器Manim简介
2019/08/07 Python
django 数据库连接模块解析及简单长连接改造方法
2019/08/29 Python
django实现用户注册实例讲解
2019/10/30 Python
python调用接口的4种方式代码实例
2019/11/19 Python
Shoes For Crews法国官网:美国领先的防滑鞋设计和制造商
2018/01/01 全球购物
医学检验专业个人求职信范文
2013/12/04 职场文书
大学生入党积极分子党校学习思想汇报
2014/10/25 职场文书
领导欢迎词范文
2015/01/26 职场文书
消防安全培训工作总结
2015/10/23 职场文书
《曾国藩家书》读后感——读家书,立家风
2019/08/21 职场文书
创业计划书之服装
2019/10/07 职场文书
python正则表达式re.search()的基本使用教程
2021/05/21 Python
Python多线程实用方法以及共享变量资源竞争问题
2022/04/12 Python