解决await在forEach中不起作用的问题
一、前言
前两天在项目中用for遍历的时候遇到了一个坑,花了一天的时间解决。这里就记一下。
二、问题
首先引一个很简单题目:给一个数组,每隔1s打印出来.这里我把我一开始在项目中的代码贴出来.(当然这里完全和业务无关的)
const_=require('lodash'); constecho=async(i)=>{ setTimeout(()=>{ console.log('i===>',i); },5000); } letarrs=[1,2,3,4,5,6,7,8,9,10]; consttask=async()=>{ _.forEach(arrs,async(i)=>{ awaitecho(i); }) } construn=async()=>{ console.log('run-start====>date:',newDate().toLocaleDateString()) awaittask(); console.log('run-end====>date:',newDate().toLocaleDateString()) } (async()=>{ console.log('start...') awaitrun(); 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'); constecho=async(i)=>{ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ console.log('i===>',i,newDate().toLocaleTimeString()); resolve(i); },2000); }) } letarrs=[1,2,3,4,5,6,7,8,9,10]; consttask=async()=>{ //_.forEach(arrs,async(i)=>{ //awaitecho(ji); //}) //arrs.forEach(async(i)=>{ //awaitecho(i); //}); for(constiofarrs){ awaitecho(i); } } construn=async()=>{ console.log('run-start====>date:',newDate().toLocaleDateString()) awaittask(); console.log('run-end====>date:',newDate().toLocaleDateString()) } (async()=>{ console.log('start...') awaitrun(); console.log('end...') })() //输出 start... run-start====>date:2018-8-26 i===>120:51:29 i===>220:51:31 i===>320:51:33 i===>420:51:35 i===>520:51:37 i===>620:51:39 i===>720:51:42 i===>820:51:44 i===>920:51:46 i===>1020:51:48 run-end====>date:2018-8-26 end...
三、总结
当解决问题的时候,有时候可以使用排除法,比方说在这个例子中,我们知道await这个机制肯定是没问题的,如果真的有问题肯定不会轮到我测出来,那么其实剩下来的问题只能是for遍历的原因了.
因为我一开始是用lodash实现的,那么就可以想是不是lodash的forEach没有作(或者做了多余)await处理,此时就可以换种方式试试了,总的来说还是经验的问题吧。
补充:在forEach中使用async/await遇到的问题
一、问题描述
前几天,项目中遇到一个JavaScript异步问题:
有一组数据,需要对每一个数据进行一个异步处理,并且希望处理的时候是同步的。
用代码描述如下:
//生成数据 constgetNumbers=()=>{ returnPromise.resolve([1,2,3]) } //异步处理 constdoMulti=num=>{ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ if(num){ resolve(num*num) }else{ reject(newError('numnotspecified')) } },2000) }) } //主函数 constmain=async()=>{ console.log('start'); constnums=[1,2,3]; nums.forEach(async(x)=>{ constres=awaitdoMulti(x); console.log(res); }); console.log('end'); }; //执行 main();
在这个例子中,通过forEach遍历地将每一个数字都执行doMulti操作。代码执行的结果是:首先会立即打印start、end。2秒后,一次性输出1,4,9。
这个结果和我们的预期有些区别,我们是希望每间隔2秒,执行一次异步处理,依次输出1,4,9。所以当前代码应该是并行执行了,而我们期望的应该是串行执行。
我们尝试把forEach循环替换成for循环:
constmain=async()=>{ console.log('start'); constnums=awaitgetNumbers(); for(constxofnums){ constres=awaitdoMulti(x); console.log(res); } console.log('end'); };
执行结果完全符合了预期:依次输出:start、1,4,9,end。
二、问题分析
思路都是一样的,只是使用的遍历方式不一样而已,为什么会出现这样的情况呢?在MDN上查找了一下forEach的polyfill参考MDN-Array.prototype.forEach():
//ProductionstepsofECMA-262,Edition5,15.4.4.18 //Reference:http://es5.github.io/#x15.4.4.18 if(!Array.prototype.forEach){ Array.prototype.forEach=function(callback,thisArg){ varT,k; if(this==null){ thrownewTypeError('thisisnullornotdefined'); } //1.LetObetheresultofcallingtoObject()passingthe //|this|valueastheargument. varO=Object(this); //2.LetlenValuebetheresultofcallingtheGet()internal //methodofOwiththeargument"length". //3.LetlenbetoUint32(lenValue). varlen=O.length>>>0; //4.IfisCallable(callback)isfalse,throwaTypeErrorexception. //See:http://es5.github.com/#x9.11 if(typeofcallback!=="function"){ thrownewTypeError(callback+'isnotafunction'); } //5.IfthisArgwassupplied,letTbethisArg;elselet //Tbeundefined. if(arguments.length>1){ T=thisArg; } //6.Letkbe0 k=0; //7.Repeat,whilek从上面的polyfill中的setp7,我们可以简单地理解成下面的步骤:
Array.prototype.forEach=function(callback){ //thisrepresentsourarray for(letindex=0;index相当于for循环执行了这个异步函数,所以是并行执行,导致了一次性全部输出结果:1,4,9。
constmain=async()=>{ console.log('start'); constnums=awaitgetNumbers(); //nums.forEach(async(x)=>{ //constres=awaitdoMulti(x); //console.log(res); //}); for(letindex=0;index{ constres=awaitdoMulti(x) console.log(res) })(nums[index]) } console.log('end'); }; 三、解决方案
现在,我们把问题分析清楚了。前面用for-of循环来代替forEach作为解决方案,其实我们也可以改造一下forEach:
constasyncForEach=async(array,callback)=>{ for(letindex=0;index{ console.log('start'); constnums=awaitgetNumbers(); awaitasyncForEach(nums,asyncx=>{ constres=awaitdoMulti(x) console.log(res) }) console.log('end'); }; main(); 四、Eslint问题
这时候Eslint又报了错:no-await-in-loop。关于这一点,Eslint官方文档https://eslint.org/docs/rules/no-await-in-loop也做了说明。
好的写法:
asyncfunctionfoo(things){ constresults=[]; for(constthingofthings){ //Good:allasynchronousoperationsareimmediatelystarted. results.push(bar(thing)); } //Nowthatalltheasynchronousoperationsarerunning,herewewaituntiltheyallcomplete. returnbaz(awaitPromise.all(results)); }不好的写法:
asyncfunctionfoo(things){ constresults=[]; for(constthingofthings){ //Bad:eachloopiterationisdelayeduntiltheentireasynchronousoperationcompletes results.push(awaitbar(thing)); } returnbaz(results); }其实上面两种写法没有什么好坏之分,这两种写法的结果是完全不一样的。Eslint推荐的“好的写法”在执行异步操作的时候没有顺序的,“不好的写法”中有顺序,具体需要用哪种写法还是要根据业务需求来决定。
所以,在文档的WhenNotToUseIt中,Eslint也提到,如果需要有顺序地执行,我们是可以禁止掉该规则的:
Inmanycasestheiterationsofalooparenotactuallyindependentofeach-other.Forexample,theoutputofoneiterationmightbeusedastheinputtoanother.Or,loopsmaybeusedtoretryasynchronousoperationsthatwereunsuccessful.Or,loopsmaybeusedtopreventyourcodefromsendinganexcessiveamountofrequestsinparallel.InsuchcasesitmakessensetouseawaitwithinaloopanditisrecommendedtodisabletheruleviaastandardESLintdisablecomment.
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。