徹底理解setTimeout()

之前在網(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末躺屁,一起剝皮案震驚了整個濱河市肯夏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌犀暑,老刑警劉巖驯击,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異耐亏,居然都是意外死亡徊都,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門广辰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暇矫,“玉大人主之,你說我怎么就攤上這事±罡” “怎么了槽奕?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長房轿。 經(jīng)常有香客問我粤攒,道長,這世上最難降的妖魔是什么囱持? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任夯接,我火速辦了婚禮,結(jié)果婚禮上纷妆,老公的妹妹穿的比我還像新娘盔几。我一直安慰自己,他們只是感情好凭需,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布问欠。 她就那樣靜靜地躺著,像睡著了一般粒蜈。 火紅的嫁衣襯著肌膚如雪顺献。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天枯怖,我揣著相機與錄音注整,去河邊找鬼。 笑死度硝,一個胖子當(dāng)著我的面吹牛肿轨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蕊程,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼椒袍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了藻茂?” 一聲冷哼從身側(cè)響起驹暑,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辨赐,沒想到半個月后优俘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡掀序,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年帆焕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片不恭。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡叶雹,死狀恐怖财饥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情折晦,我是刑警寧澤佑力,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站筋遭,受9級特大地震影響打颤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漓滔,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一编饺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧响驴,春花似錦透且、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至琳骡,卻和暖如春锅论,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背楣号。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工最易, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炫狱。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓藻懒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親视译。 傳聞我的和親對象是個殘疾皇子嬉荆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 弄懂js異步 講異步之前,我們必須掌握一個基礎(chǔ)知識-event-loop酷含。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,712評論 0 5
  • 你不知道JS:異步 第三章:Promises 在第二章鄙早,我們指出了采用回調(diào)來表達異步和管理并發(fā)時的兩種主要不足:缺...
    purple_force閱讀 2,070評論 0 4
  • 特別說明,為便于查閱第美,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 890評論 0 2
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持蝶锋,譯者再次奉上一點點福利:阿里云產(chǎn)品券陆爽,享受所有官網(wǎng)優(yōu)惠什往,并抽取幸運大...
    HetfieldJoe閱讀 11,026評論 26 95
  • 編后吐槽:寫的快花眼,很詳細慌闭,耐心看必受益匪淺 JavaScript的執(zhí)行環(huán)境是「單線程」的别威。所謂單線程躯舔,是指JS...
    果汁涼茶丶閱讀 4,633評論 8 27