在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
一、前言前两天在项目中用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 也提到,如果需要有顺序地执行,我们是可以禁止掉该规则的:
以上为个人经验,希望能给大家一个参考,也希望大家多多支持极客世界。如有错误或未考虑完全的地方,望不吝赐教。 |
请发表评论