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

一、前言

前两天在项目中用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.

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

(0)

相关推荐

  • async/await让异步操作同步执行的方法详解

    一.前言 我们经常会遇到这样的麻烦事,多个函数按顺序执行,返回结果却不是我们预期的顺序,原因一般是由于异步操作引起的,所以呢,我们需要一种解决方案来处理这种问题,从而使得异步操作按照同步的方式来执行,这样我们就可以控制异步操作输出结果的顺序了 二.异步操作会带来什么问题 异步操作可能会许多的问题,下面是常见的两种 1.函数执行的结果并不是按照顺序返回 function fn1(){ console.log(111) setTimeout(function(){ console.log('wait

  • 解决mybatis批量更新(update foreach)失败的问题

    如下所示: <!--批量更新报表 --> <update id="updateIssueByBatch" parameterType="java.util.List"> <foreach collection="issueList" item="item" index="index" separator=";"> update sys_issue &l

  • JavaScript forEach中return失效问题解决方案

    在最近学习并使用ES6过程中,在一次循环数组并且作判断时返回值无效,return拿到的结果是undefined 我们分别使用for循环和forEach方法来验证: 结果:for循环里的可以return终止遍历,forEach里的return不能终止遍历 以上是return终止for循环的遍历和forEach循环的遍历测试. -------------------------------------------------------------- 分 割 线 ------------------

  • 解决mybatis使用foreach批量insert异常的问题

    异常 org.springframework.jdbc.BadSqlGrammarException: ### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version fo

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

    一.前言 前两天在项目中用for遍历的时候遇到了一个坑,花了一天的时间解决.这里就记一下. 二.问题 首先引一个很简单题目:给一个数组,每隔1s打印出来.这里我把我一开始在项目中的代码贴出来.(当然这里完全和业务无关的) const _ = require('lodash'); const echo = async (i) => { setTimeout(() => { console.log('i===>', i); }, 5000); } let arrs = [1, 2, 3, 4

  • 重学 JS:为啥 await 不能用在 forEach 中详解

    这是重学 JS 系列的第三篇文章,写这个系列的初衷也是为了夯实自己的 JS 基础或者了解一些之前不知道的东西.既然是重学,肯定不会从零开始介绍一个知识点,如有遇到不会的内容请自行查找资料. 不知道你有没有写过类似的代码,反正以前我是写过 function test() { let arr = [3, 2, 1] arr.forEach(async item => { const res = await fetch(item) console.log(res) }) console.log('en

  • PHP的foreach中使用引用时需要注意的一个问题和解决方法

    一.问题先看一个例子: <?php$ar = array(1, 2, 3);var_dump($ar);foreach ($ar as &$v) {}foreach ($ar as $v) {}var_dump($ar);?>输出为: array(3) {  [0]=>  int(1)  [1]=>  int(2)  [2]=>  int(3)}array(3) {  [0]=>  int(1)  [1]=>  int(2)  [2]=>  &

  • 解决jquery appaend元素中id绑定事件失效的问题

    1. 在jquery中append元素,如果该元素中有id值并且绑定事件,那么该id事件会失效,必须刷新一下才能使用. 2.解决办法: 举例:如果在一个<div class="title"></div>中,通过append添加一个id="demo"的按钮,常用的直接$("Selector").on("eventType",function(){})监听事件函数不起作用,例如该按钮的点击监听事件应该这样写

  • 深入理解javascript中defer的作用

    很多人都已经把 Javascript的用的炉火纯青了,但见到defer未必就知道他是做什么用的:很多人也都遇到过这样的问题,需要直接执行别且操作DOM对象的js 总是报找不到对象的错误,原因大家也都知道就是页面还有没有加载完毕,js的操作对象还在下载中.但很多人都不知道,添加defer标签就能轻而易举的解决这个问题. <script src="../CGI-bin/delscript.js" defer></script> 中的defer作用是文档加载完毕了再执

  • 解决django 向mysql中写入中文字符出错的问题

    之前使用django+mysql建立的一个站点,发现向数据库中写入中文字符时总会报错,尝试了修改settings文件和更改数据表的字符集后仍不起作用.最后发现,在更改mysql的字符集后,需要重建数据库,才能起作用. 这里完整记录一下解决方案 首先更改mysql的字符集 ubuntu下找到/etc/mysql/my.cnf   在最后添加 [mysqld] character-set-server=utf8 [client] default-character-set=utf8 [mysql]

  • 解决idea使用过程中让你觉得不爽的一些问题(小结)

    每次导入一个新的 Project 项目的时候已经配置好的 maven 被还原为默认状态 如果 idea 的版本比较老的话看到的是这样的 然后再修改 Maven 配置就可以了 2.在创建或者导入 Maven 项目后,编写代码的时候总是提示不支持 Java8,编译也无法通过 我们查看 settings -> java compiler 发现编译版本是 1.8 之前的版本 解决方案:在 pom.xml 中加入 JDK 版本 <properties> <java.version>1.

  • Vue中key的作用示例代码详解

    Vue中key的作用 key的特殊attribute主要用在Vue的虚拟DOM算法,在新旧Nodes对比时辨识VNodes.如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改.复用相同类型元素的算法,而使用key时,它会基于key的变化重新排列元素顺序,并且会移除key不存在的元素.此外有相同父元素的子元素必须有独特的key,重复的key会造成渲染错误. 描述 首先是官方文档的描述,当Vue正在更新使用v-for渲染的元素列表时,它默认使用就地更新的策略,如果数据项的

  • Mybatis #foreach中相同的变量名导致值覆盖的问题解决

    目录 背景 问题原因(简略版) Mybatis流程源码解析(长文警告,按需自取) 一.获取SqlSessionFactory 二.获取SqlSession 三.执行SQL 背景 使用Mybatis中执行如下查询: 单元测试 @Test public void test1() { String resource = "mybatis-config.xml"; InputStream inputStream = null; try { inputStream = Resources.get

随机推荐