参考文献:王仕军——知乎专栏前端周刊

感谢作者的热心总结,本文在理解的基础上,根据自己能力水平作了一点小小的修改,在加深自己印象的同时也希望能和各位共同进步...

 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);
})();
03-06 21:43