JavaScript setTimeout要理解( 三 )


5)、再继续走,执行到第二个setTimeout,发现是宏任务,派发它的回调到上面setTimeout类型的宏任务队列中去 。
6)、再走到最后一个new Promise,很明显,这里会有第四个输出:"promise2",然后它的then中的回调也会被派发到上面的Promise.then类型的微任务队列中去 。
7)、第一轮事件循环的宏任务执行完成(整体代码可以看做宏任务) 。此时微任务队列中只有一个Promise.then类型微任务队列,它里面有两个任务 。宏任务队列中也只有一个setTimeout类型的宏任务队列 。
8)、下面执行第一轮事件循环的微任务,很明显,会分别打印出"then1",和"then2" 。分别是第五和第六个输出 。此时第一轮事件循环完成 。
9)、开始第二轮事件循环:执行setTimeout类型队列(宏任务队列)中的所有任务 。发现都有延时,但延时最短的是for循环中第一次循环push进来的那个setTimeout和上面第5个步骤中的第二个setTimeout,它们都只延时1s 。它们会被同时执行,但前者先被push进来,所以先执行它!它的作用就是打印变量i,在当前作用域找变量i,木有!去它上层作用域(这里是全局作用域)找,找到了,但此时的i早已是6了 。(为啥不是5,那你得去补补for循环的执行流程了~)所以这里第七个输出是延时1s后打印出6 。
10)、紧接着执行第二个setTimeout,它会先后打印出"timeout2"和"timeout2_promise",这分别是第八和第九个输出 。但这里发现了then,又把它push到上面已经被执行完的then队列中去 。
11)、这里要注意,因为出现了微任务then队列,所以这里会执行该队列中的所有任务(此时只有一个任务),即打印出"timeout2_then" 。这是第十个输出
11)、继续回过头来执行宏任务队列,此时是执行延时为2s的第一个setTimeout和for循环中第二次循环的那个setTimeout,跟上面一样,前者是第一个被push进来的,所以它先执行 。这里会延时1秒(原因下面会解释)分别输出“timeout1”和“timeout1_promise”,但发现了里面也有一个then,于是push到then微任务队列并立即执行,输出了"timeout1_then" 。紧接着执行for中第二次循环的setTimeout,输出6 。注意这三个几乎是同时被打印出来的 。他们分别是第十一到十三个输出 。
12)、再就很简单了,把省下的for循环中后面三次循环被push进来的setTimeout依次执行,于是每隔1s输出一个6,连续输出3次 。
13)、第二轮事件循环结束,全部代码执行完毕 。
global12345promise1promise2then1then2//延迟1s6timeout2timeout2_promisetimeout2_then//延迟1stimeout117 timeout1_promise20 timeout1_then6//每隔1s输出3个6这里解释下为什么上面第11步不是延迟2秒再输出“timeout1”和“timeout1_promise”,这时需要理解setTimeout()延时参数的意思,这个延迟时间始终是相对主程序执行完毕的那个时间算的 ,并且多个setTimeout执行的先后顺序也是由这个延迟时间决定的 。
再回过头来看上面那个问题,理解了事件循环的机制,问题就很简单了 。for循环时setTimeout()不是立即执行的,它们的回调被push到了宏任务队列当中,而在执行任务队列里的回调函数时,变量i早已变成了6 。那如何得到想要的结果呢?很简单,原理就是需要给循环中的setTimeout()创建一个闭包作用域,让它执行的时候找到的变量i是正确的 。

知道了原理,解决方案就很多了,下面给出5种方案,
1)引入IIFE
for(var i = 0;i<5;i ++) { (function(i){ setTimeout(function timer() { console.log(i) }, i * 1000); })(i);}2)利用ES 6引入的let关键字
for(let i = 0;i<5;i++) { setTimeout(function timer(){ console.log(i); }, i * 1000);}for 循环头部的let 声明还会有一个特殊的行为 。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明 。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量 。
3)利用ES 5引入的bind函数
for (var i=1; i<=5; i++) { setTimeout( function timer(i) { console.log(i); }.bind(null,i), i*1000 );}4)利用setTimeout第三个参数
for (var i=1; i<=5; i++) { setTimeout( function timer(i) { console.log(i);}, i*1000,i );}
注:setTimeout函数第三个参数及以后的参数都可以作为timer函数的参数 。
5)把setTimeout用一个方法单独出来形成闭包
var loop = function (i) { setTimeout(function timer() { console.log(i);}, i*1000);};for (var i = 1;i <= 5; i++) { loop(i);}
【JavaScript setTimeout要理解】


推荐阅读