参考文献:王仕军——知乎专栏前端周刊
感谢作者的热心总结,本文在理解的基础上,根据自己能力水平作了一点小小的修改,在加深自己印象的同时也希望能和各位共同进步...
1. 异步与for循环
抛出一个问题,下面的代码输出什么?
1 for (var i = 0; i < 5; i++) { 2 setTimeout(function() { 3 console.log(i); 4 }, 1000); 5 } 6 console.log(i);
相信绝大部分同学都能答的上,它的正确答案是立即输出5,过1秒钟后一次性输出5个5,这是一个典型的JS异步问题,首先for循环的循环体是一个异步函数,并且变量i添加到全局环境中,所以立即输出一个5,一秒钟后,异步函数setTimeout输出五次循环的结果,打印5 5 5 5 5(没有时间间隔)。
2. 闭包
现在我们把需求改一下,希望输出的结果是5 ->0,1,2,3,4, 应该怎么修改代码呢?
很明显我们可以用闭包创建一个不销毁的作用域,保证变量i每次都能正常输出。
1 for(var i=0;i<5;i++){ 2 (function(j) 3 {setTimeout(() => { 4 console.log(j); //过一秒输出 0,1,2,3,4 5 }, 1000)})(i) 6 } 7 console.log(i); //立即输出5
因为立即执行会造成内存泄漏不建立大量使用,那么我们还可以这样
var output = function(i){ setTimeout(()=>{ console.log(i); // 过1秒输出0,1,2,3,4 },1000) } for(var i=0;i<5;i++){ output(i); } console.log(i); //立即输出5
JS基本类型是按值传递的,我们给函数output传了一个参数,所以它就会保存每次循环的实参,所以得到的结果和采用立即执行函数的结果一致。
3. ES6语法
当然我们也可以使用ES6的语法,还记得for循环中使用let声明可以有效阻止变量添加到全局作用域吗?
1 for(let i=0;i<5;i++){ 2 setTimeout(()=>{ 3 console.log(i) //一秒钟后同时输出0,1,2,3,4 4 },1000) 5 }
6 console.log(i) //这一行会报错,因为i只存在于for循环中
for循环中let声明有一个特点,i只在本轮循环中有效,所以每循环一个i其实都是新变量,而javaScript引擎内部会记住上一次循环的值,初始化变量i时,就在上轮循环基础上计算。
现在我们又改一下需求,希望先输出0,之后每隔一秒依次输出1,2,3,4,循环结束再输出5。
很容易想到,我们可以再增加一个定时器,定时器的时间和循环次数有关
1 for(var i=0;i<5;i++){ 2 (function(j){ 3 setTimeout(() => { 4 console.log(j) //立即输出0,之后每隔1秒输出1,2,3,4 5 }, 1000*j); 6 })(i) 7 } 8 setTimeout(()=>{ 9 console.log(i) //循环结束输出5 10 },1000*i)
这虽然也是个办法,但代码写着确实不太好看,异步操作我们首先就要想到Promise对象,尝试用Promise对象来改写
let tasks = []; for(var i=0;i<5;i++){ ((j)=>{ tasks.push(new Promise( (resolve)=>{ setTimeout(() => { console.log(j); resolve(); //执行resolve,返回Promise处理结果 }, 1000*j); } )) })(i) } Promise.all(tasks).then(()=>{ setTimeout(() => { console.log(i); }, 1000); //只要把时间设为1秒 })
Promise.all返回一个Promise实例,在tasks的promise状态为resolved时回调完成,这就是我们必须要在循环体中resolve()的原因。
我们将上面的代码重新排版,让其颗粒度更小,模块化更好,简洁明了
let tasks = []; //存放一个异步操作 let output = (i)=> //返回一个Promise对象 new Promise((resolve)=>{ setTimeout(() => { console.log(i); resolve(); }, 1000*i); }) for(var i=0;i<5;i++){ //生成全部的异步操作 tasks.push(output(i)) } Promise.all(tasks).then(()=>{ //tasks里的promise对象都为resolved调用then链的第一个回调函数 setTimeout(() => { console.log(i) }, 1000); })
4. async/await优化
上次写了一篇关于async和await优化then链的博客,感兴趣的可以看看:深入理解async/await
对于then链,我们是可以进一步优化的:
let sleep = (timeountMS) => new Promise((resolve) => { setTimeout(resolve, timeountMS); }); (async () => { // 声明即执行的 async 函数表达式 for (var i = 0; i < 5; i++) { await sleep(1000); console.log(i); } await sleep(1000); console.log(i); })();