Solve the problem that await does not work in forEach

  • 2021-10-27 06:24:04
  • OfStack

1. Preface

Two days ago, when traversing with for in the project, I encountered a pit and spent one day solving it. Just write down one here.

2. Problems

Let's start with a very simple question: Give an array and print it out every 1s. Here I post the code I started in the project with 1. (Of course, this is completely unrelated to the business.)


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

The above code and output have been given, very strange, here await does not have its effect. 1 Beginning because is added business, is my business code went wrong, and then I took the code out, or did not work, at that time I was really on await doubt.

Finally, the answer to the question is given:

forEach and []. forEach do not support await. If await must be executed while traversing 1, for-of can be used

Here is the correct code:


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...')
})()
//  Output 
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...

3. Summary

When solving problems, we can sometimes use the exclusion method. For example, in this example, we know that the mechanism of await is definitely no problem. If there is a problem, it will definitely not be my turn to measure it. In fact, the remaining problems can only be the reason of for traversal.

Because I started with lodash, then I can think about whether forEach of lodash did not do (or did redundant) await processing. At this time, I can try it in another way. Generally speaking, it is still a matter of experience.

Supplement: Problems encountered in using async/await in forEach

1. Description of the problem

A few days ago, I encountered an JavaScript asynchronous problem in my project:

There is a set of data, and every data needs to be processed asynchronously, and it is expected that the processing will be synchronous.

Described in code as follows:


//  Generate data 
const getNumbers = () => {
 return Promise.resolve([1, 2, 3])
}
//  Asynchronous processing 
const doMulti = num => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if (num) {
    resolve(num * num)
   } else {
    reject(new Error('num not specified'))
   }
  }, 2000)
 })
}
//  Principal function 
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');
};
//  Execute 
main();

In this example, doMulti is iteratively performed for every 1 digit through forEach. The result of code execution is that start and end will be printed immediately. After 2 seconds, 1, 4, 9 are output once.

This result is somewhat different from what we expected. We want to perform asynchronous processing every 2 seconds, and output 1, 4 and 9 in turn. Therefore, the current code should be executed in parallel, and what we expect should be serial execution.

We try to replace the forEach loop with the for loop:


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

The execution results are completely in line with expectations: start, 1, 4, 9 and end are output in turn.

2. Problem analysis

The train of thought is 1 kind, but the traversal method used is not 1 kind. Why does this happen? Find polyfill reference MDN-Array. prototype. forEach () under 1 forEach on MDN:


// 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
 };
}

From setp 7 in polyfill above, we can simply understand the following steps:


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

The equivalent for loop executes this asynchronous function, so it is executed in parallel, resulting in 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');
};

3. Solutions

Now, we have analyzed the problem clearly. In the past, for-of cycle was used instead of forEach as a solution. In fact, we can also transform forEach under 1:


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();

4. Eslint issues

At this time, Eslint reported an error again: no-await-in-loop. This point is also explained in the official document https://eslint.org/docs/rules/no-await-in-loop.

Good writing:


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

Bad writing:


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

In fact, there is no difference between the above two writing methods, and the results of these two writing methods are completely different. The "good writing" recommended by Eslint has no order when performing asynchronous operations, but there is order in the "bad writing". Which writing method needs to be used should be decided according to business requirements.

Therefore, in the document When Not To Use It, Eslint also mentioned that we can disable this rule if we need to execute it sequentially:

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.


Related articles: