之前在網(wǎng)上看了很多關(guān)于setTimeout的文章,但我感覺都只是點到為止,并沒有較深入的去剖析带膀,也可能是我腦袋瓜笨扇救,不容易被點解刑枝。后面看了《你不知道的javascript-上卷》一書,決定重新再來理一次迅腔。這次我覺得我應(yīng)該整明白了装畅。于是分享給大家,文中解釋有錯誤的部分還希望大家留言指正沧烈。
首先我們還是來看那道大家再熟悉不過的前端面試題:
for (var i = 1;i <= 5;i ++) {
setTimeout(function timer() {
console.log(i)
},i * 1000)
}
我想剛?cè)腴T的童鞋或者對JS作用域掠兄、閉包以及事件循環(huán)等概念不了解的童鞋會想當(dāng)然的認(rèn)為這道題的答案應(yīng)該是:
第一次循環(huán),隔一秒輸出1锌雀;
第二次循環(huán)蚂夕,隔兩秒輸出2;
第三次循環(huán)腋逆,隔三秒輸出3婿牍;
第四次循環(huán),隔四秒輸出4惩歉;
第五次循環(huán)牍汹,隔五秒輸出5;
或者還有同學(xué)預(yù)期的結(jié)果是分別輸出數(shù)字1~5柬泽,每秒一次慎菲,每次一個。
但實際結(jié)果大家去控制臺打印了都知道:以一秒的頻率連續(xù)輸出五個6锨并!
相信對于很多童鞋第一次看到這個結(jié)果是懵的露该,包括我第一次看到結(jié)果是懵逼的!
然而還沒等你反應(yīng)過來第煮,面試官又要求你改動一下代碼解幼,要它以一秒的頻率分別輸出1,2包警,3撵摆,4,5害晦。如果你不了解或者沒有深入理解JS中的作用域特铝、閉包以及事件循環(huán),那么就可以和面試官說拜拜了。
這道題涉及到的知識點我上面已經(jīng)提到過兩次鲫剿,這里我們還是先簡單地過一下這些知識點:
1鳄逾、作用域:這里我引用《你不知道的javascript》中的一個比喻,可以把作用域鏈想象成一座高樓灵莲,第一層代表當(dāng)前執(zhí)行作用域雕凹,樓的頂層代表全局作用域。我們在查找變量時會先在當(dāng)前樓層進行查找政冻,如果沒有找到枚抵,就會坐電梯前往上一層樓,如果還是沒有找到就繼續(xù)向上找明场,以此類推俄精。到達頂層后(全局作用域),可能找到了你所需的變量榕堰,也可能沒找到,但無論如何查找過程都將停止嫌套。
2逆屡、閉包:我的理解是在傳遞函數(shù)類型的變量時,該函數(shù)會保留定義它的所在函數(shù)的作用域踱讨。讀起來可能比較繞魏蔗,或者可以簡單的這么理解,A函數(shù)中定義了B函數(shù)并且它返回了B函數(shù)痹筛,那么不管B函數(shù)在哪里被調(diào)用或如何被調(diào)用莺治,它都會保留A函數(shù)的作用域。
3帚稠、事件循環(huán):這個概念深入起來很復(fù)雜谣旁,下面新開一個段落只說一些跟本文相關(guān)的內(nèi)容。
說起事件循環(huán)滋早,不得不提起任務(wù)隊列榄审。事件循環(huán)只有一個,但任務(wù)隊列可能有多個杆麸,任務(wù)隊列可分為宏任務(wù)(macro-task)和微任務(wù)(micro-task)搁进。XHR回調(diào)、事件回調(diào)(鼠標(biāo)鍵盤事件)昔头、setImmediate饼问、setTimeout、setInterval揭斧、indexedDB數(shù)據(jù)庫操作等I/O以及UI rendering都屬于宏任務(wù)(也有文章說UI render不屬于宏任務(wù)莱革,目前還沒有定論),process.nextTick、Promise.then驮吱、Object.observer(已經(jīng)被廢棄)茧妒、MutationObserver(html5新特性)屬于微任務(wù)。注意進入到任務(wù)隊列的是具體的執(zhí)行任務(wù)的函數(shù)左冬。比如上述例子setTimeout()中的timer函數(shù)桐筏。另外不同類型的任務(wù)會分別進入到他們所屬類型的任務(wù)隊列,比如所有setTimeout()的回調(diào)都會進入到setTimeout任務(wù)隊列拇砰,所有then()回調(diào)都會進入到then隊列梅忌。當(dāng)前的整體代碼我們可以認(rèn)為是宏任務(wù)。事件循環(huán)從當(dāng)前整體代碼開始第一次事件循環(huán)除破,然后再執(zhí)行隊列中所有的微任務(wù)牧氮,當(dāng)微任務(wù)執(zhí)行完畢之后,事件循環(huán)再找到其中一個宏任務(wù)隊列并執(zhí)行其中的所有任務(wù)瑰枫,然后再找到一個微任務(wù)隊列并執(zhí)行里面的所有任務(wù)踱葛,就這樣一直循環(huán)下去。這就是我所理解的事件循環(huán)光坝。來尸诽,還是看個栗子:
console.log('global')
setTimeout(function () {
console.log('timeout1')
new Promise(function (resolve) {
console.log('timeout1_promise')
resolve()
}).then(function () {
console.log('timeout1_then')
})
},2000)
for (var i = 1;i <= 5;i ++) {
setTimeout(function() {
console.log(i)
},i*1000)
console.log(i)
}
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('then1')
})
setTimeout(function () {
console.log('timeout2')
new Promise(function (resolve) {
console.log('timeout2_promise')
resolve()
}).then(function () {
console.log('timeout2_then')
})
}, 1000)
new Promise(function (resolve) {
console.log('promise2')
resolve()
}).then(function () {
console.log('then2')
})
我們來一步一步分析以上代碼:
1)、首先執(zhí)行整體代碼盯另,“global”會被第一個打印出來性含。這是第一個輸出.
2)、執(zhí)行到第一個setTimeout時鸳惯,發(fā)現(xiàn)它是宏任務(wù)商蕴,此時會新建一個setTimeout類型的宏任務(wù)隊列并派發(fā)當(dāng)前這個setTimeout的回調(diào)函數(shù)到剛建好的這個宏任務(wù)隊列中去,并且輪到它執(zhí)行時要延遲2秒后再執(zhí)行芝发。
3)绪商、代碼繼續(xù)執(zhí)行走到for循環(huán),發(fā)現(xiàn)是循環(huán)5次setTimeout()辅鲸,那就把這5個setTimeout中的回調(diào)函數(shù)依次派發(fā)到上面新建的setTimeout類型的宏任務(wù)隊列中去部宿,注意,這5個setTimeout的延遲分別是1到5秒瓢湃。此時這個setTimeout類型的宏任務(wù)隊列中應(yīng)該有6個任務(wù)了理张。再執(zhí)行for循環(huán)里的console.log(i),很簡單绵患,直接輸出1,2,3,4,5雾叭,這是第二個輸出。
4)落蝙、再執(zhí)行到new Promise织狐,Promise構(gòu)造函數(shù)中的第一個參數(shù)在new的時候會直接執(zhí)行暂幼,因此不會進入任何隊列,所以第三個輸出是"promise1"移迫,上面有說到Promise.then是微任務(wù)旺嬉,那么這里會生成一個Promise.then類型的微任務(wù)隊列,這里的then回調(diào)會被push進這個隊列中厨埋。
5)邪媳、再繼續(xù)走,執(zhí)行到第二個setTimeout荡陷,發(fā)現(xiàn)是宏任務(wù)雨效,派發(fā)它的回調(diào)到上面setTimeout類型的宏任務(wù)隊列中去。
6)废赞、再走到最后一個new Promise徽龟,很明顯,這里會有第四個輸出:"promise2"唉地,然后它的then中的回調(diào)也會被派發(fā)到上面的Promise.then類型的微任務(wù)隊列中去据悔。
7)、第一輪事件循環(huán)的宏任務(wù)執(zhí)行完成(整體代碼可以看做宏任務(wù))耘沼。此時微任務(wù)隊列中只有一個Promise.then類型微任務(wù)隊列极颓,它里面有兩個任務(wù)。宏任務(wù)隊列中也只有一個setTimeout類型的宏任務(wù)隊列耕拷。
8)、下面執(zhí)行第一輪事件循環(huán)的微任務(wù)托享,很明顯骚烧,會分別打印出"then1",和"then2"闰围。分別是第五和第六個輸出赃绊。此時第一輪事件循環(huán)完成。
9)羡榴、開始第二輪事件循環(huán):執(zhí)行setTimeout類型隊列(宏任務(wù)隊列)中的所有任務(wù)碧查。發(fā)現(xiàn)都有延時,但延時最短的是for循環(huán)中第一次循環(huán)push進來的那個setTimeout和上面第5個步驟中的第二個setTimeout校仑,它們都只延時1s忠售。它們會被同時執(zhí)行,但前者先被push進來迄沫,所以先執(zhí)行它稻扬!它的作用就是打印變量i,在當(dāng)前作用域找變量i羊瘩,木有泰佳!去它上層作用域(這里是全局作用域)找盼砍,找到了,但此時的i早已是6了逝她。(為啥不是5浇坐,那你得去補補for循環(huán)的執(zhí)行流程了~)所以這里第七個輸出是延時1s后打印出6。
10)黔宛、緊接著執(zhí)行第二個setTimeout近刘,它會先后打印出"timeout2"和"timeout2_promise",這分別是第八和第九個輸出宁昭。但這里發(fā)現(xiàn)了then跌宛,又把它push到上面已經(jīng)被執(zhí)行完的then隊列中去。
11)积仗、這里要注意疆拘,因為出現(xiàn)了微任務(wù)then隊列,所以這里會執(zhí)行該隊列中的所有任務(wù)(此時只有一個任務(wù))寂曹,即打印出"timeout2_then"哎迄。這是第十個輸出。
11)隆圆、繼續(xù)回過頭來執(zhí)行宏任務(wù)隊列漱挚,此時是執(zhí)行延時為2s的第一個setTimeout和for循環(huán)中第二次循環(huán)的那個setTimeout,跟上面一樣渺氧,前者是第一個被push進來的旨涝,所以它先執(zhí)行。這里會延時1秒(原因下面會解釋)分別輸出“timeout1”和“timeout1_promise”侣背,但發(fā)現(xiàn)了里面也有一個then白华,于是push到then微任務(wù)隊列并立即執(zhí)行,輸出了"timeout1_then"贩耐。緊接著執(zhí)行for中第二次循環(huán)的setTimeout弧腥,輸出6。注意這三個幾乎是同時被打印出來的潮太。他們分別是第十一到十三個輸出管搪。
12)、再就很簡單了铡买,把省下的for循環(huán)中后面三次循環(huán)被push進來的setTimeout依次執(zhí)行更鲁,于是每隔1s輸出一個6,連續(xù)輸出3次奇钞。
13)岁经、第二輪事件循環(huán)結(jié)束,全部代碼執(zhí)行完畢蛇券。
所以上代碼的執(zhí)行結(jié)果為:
global
1
2
3
4
5
promise1
promise2
then1
then2
//延遲1s
6
timeout2
timeout2_promise
timeout2_then
//延遲1s
timeout1
17 timeout1_promise
20 timeout1_then
6
//每隔1s輸出3個6
這里解釋下為什么上面第11步不是延遲2秒再輸出“timeout1”和“timeout1_promise”缀壤,這時需要理解setTimeout()延時參數(shù)的意思樊拓,這個延遲時間始終是相對主程序執(zhí)行完畢的那個時間算的 ,并且多個setTimeout執(zhí)行的先后順序也是由這個延遲時間決定的。
再回過頭來看上面那個問題塘慕,理解了事件循環(huán)的機制筋夏,問題就很簡單了。for循環(huán)時setTimeout()不是立即執(zhí)行的图呢,它們的回調(diào)被push到了宏任務(wù)隊列當(dāng)中条篷,而在執(zhí)行任務(wù)隊列里的回調(diào)函數(shù)時,變量i早已變成了6蛤织。那如何得到想要的結(jié)果呢赴叹?很簡單,原理就是需要給循環(huán)中的setTimeout()創(chuàng)建一個閉包作用域指蚜,讓它執(zhí)行的時候找到的變量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關(guān)鍵字
for(let i = 0;i<5;i++) {
setTimeout(function timer(){
console.log(i);
}, i * 1000);
}
for 循環(huán)頭部的let 聲明還會有一個特殊的行為。這個行為指出變量在循環(huán)過程中不止被聲明一次免猾,每次迭代都會聲明是辕。隨后的每個迭代都會使用上一個迭代結(jié)束時的值來初始化這個變量。
(3)利用ES 5引入的bind函數(shù)
for (var i=1; i<=5; i++) {
setTimeout( function timer(i) {
console.log(i);
}.bind(null,i), i*1000 );
}
(4)利用setTimeout第三個參數(shù)
for (var i=1; i<=5; i++) {
setTimeout( function timer(i) {
console.log(i);
}, i*1000,i );
}
注:setTimeout函數(shù)第三個參數(shù)及以后的參數(shù)都可以作為timer函數(shù)的參數(shù)猎提。
(5)把setTimeout用一個方法單獨出來形成閉包
var loop = function (i) {
setTimeout(function timer() {
console.log(i);
}, i*1000);
};
for (var i = 1;i <= 5; i++) {
loop(i);
}
好了获三,文章到此基本就結(jié)束了,第一次寫技術(shù)類的文章锨苏,肯定有不足疙教,希望大家能留言指出。最后請尊重我的勞動成果蚓炬,轉(zhuǎn)載請務(wù)必注明出處松逊。
參考:
http://www.reibang.com/p/12b9f73c5a4f
http://www.reibang.com/p/e5225ba4a025