深入理解JavaScript事件循環(huán)機制

眾所周知,JavaScript 是一門單線程語言,雖然在 html5 中提出了 Web-Worker 系馆,但這并未改變 JavaScript 是單線程這一核心⊥缯眨可看HTML規(guī)范中的這段話:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

為了協(xié)調(diào)事件由蘑、用戶交互、腳本代兵、UI 渲染和網(wǎng)絡處理等行為尼酿,用戶引擎必須使用 event loops。Event Loop 包含兩類:一類是基于 Browsing Context 植影,一種是基于 Worker 裳擎,二者是獨立運行的。

而這種考察不論是面試求職思币,還是日常開發(fā)工作句惯,我們經(jīng)常會遇到這樣的情況:給定的幾行代碼,我們需要知道其輸出內(nèi)容和順序支救。因為JavaScript是一門單線程語言,所以我們可以得出結論:

JavaScript是按照語句出現(xiàn)的順序執(zhí)行的

所以我們以為JS都是這樣的:

let a = '1';
console.log(a);
let b = '2';
console.log(b);

結果是1,2
然而實際上JS是這樣的

setTimeout(function(){
    console.log('定時器開始')
});
new Promise(function(resolve){
    console.log('馬上執(zhí)行for循環(huán)');
    for(var i = 0; i < 10000; i++){
        i == 99 &amp;&amp; resolve();
    }
}).then(function(){
    console.log('執(zhí)行then函數(shù)')
});
console.log('代碼執(zhí)行結束');

依照JS是按照語句出現(xiàn)的順序執(zhí)行這個理念拷淘,輸出結果:

//"定時器開始"
//"馬上執(zhí)行for循環(huán)"
//"執(zhí)行then函數(shù)"
//"代碼執(zhí)行結束
去chrome上驗證下各墨,結果完全不對,實際上是:
//"馬上執(zhí)行for循環(huán)"
//"代碼執(zhí)行結束"
//"執(zhí)行then函數(shù)"
//"定時器開始"
這就要求我們弄懂JavaScript的執(zhí)行機制了启涯。

1.關于JavaScript

JavaScript是一門單線程語言贬堵,在最新的HTML5中提出了Web-Worker,但JavaScript是單線程這一核心仍未改變结洼。所以一切JavaScript版的”多線程”都是用單線程模擬出來的黎做,一切JavaScript多線程都是紙老虎!

2.JavaScript事件循環(huán)

既然JS是單線程松忍,那就像只有一個窗口的銀行蒸殿,客戶需要排隊一個一個辦理業(yè)務,同理JS任務也要一個一個順序執(zhí)行瞒瘸。如果一個任務耗時過長籍胯,那么后一個任務也必須等著诈铛。那么問題來了,假如我們想瀏覽新聞爬骤,但是新聞包含的超清圖片加載很慢,難道我們的網(wǎng)頁要一直卡著直到圖片完全顯示出來莫换?因此聰明的程序員將任務分為兩類:

  • 同步任務

  • 異步任務

當我們打開網(wǎng)站時霞玄,網(wǎng)頁的渲染過程就是一大堆同步任務骤铃,比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類占用資源大耗時久的任務坷剧,就是異步任務惰爬。關于這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執(zhí)行機制听隐,所以我們用導圖來說明:

在這里插入圖片描述
  • 同步和異步任務分別進入不同的執(zhí)行”場所”补鼻,同步的進入主線程,異步的進入Event Table并注冊函數(shù)雅任。

  • 當指定的事情完成時风范,EventTable會將這個函數(shù)移入Event Queue。

  • 主線程內(nèi)的任務執(zhí)行完畢為空沪么,會去Event Queue讀取對應的函數(shù)硼婿,進入主線程執(zhí)行。

    上述過程會不斷重復禽车,也就是常說的Event Loop(事件循環(huán))寇漫。

在這里插入圖片描述

再通俗點就是同步和異步任務分別進入不同的執(zhí)行環(huán)境,同步的進入主線程殉摔,即主執(zhí)行棧州胳,異步的進入Event Queue。主線程內(nèi)的任務執(zhí)行完畢為空逸月,會去Event Queue讀取對應的任務栓撞,推入主線程執(zhí)行。 上述過程的不斷重復就是我們說的Event Loop(事件循環(huán))碗硬。

在事件循環(huán)中瓤湘,每進行一次循環(huán)操作稱為tick,通過閱讀規(guī)范可知恩尾,每一次tick的任務處理模型是比較復雜的弛说,其關鍵的步驟可以總結如下:

  • 在此次tick中選擇最先進入隊列的任務(oldest task),如果有則執(zhí)行(一次)

  • 檢查是否存在Microtasks翰意,如果存在則不停地執(zhí)行木人,直至清空Microtask Queue

  • 更新render

  • 主線程重復執(zhí)行上述步驟

可以用一張圖來說明下流程:

在這里插入圖片描述

按照上圖這種分類方式通俗點來講就是JS 的執(zhí)行機制是:

  • 執(zhí)行一個宏任務,過程中如果遇到微任務,就將其放到微任務的【事件隊列】里

  • 當前宏任務執(zhí)行完成后,會查看微任務的【事件隊列】,并將里面全部的微任務依次執(zhí)行完

這里相信有人會想問,什么是microtasks?規(guī)范中規(guī)定冀偶,task分為兩大類, 分別是Macro Task (宏任務)和Micro Task(微任務), 并且每個宏任務結束后, 都要清空所有的微任務,這里的Macro Task也是我們常說的task虎囚,有些文章并沒有對其做區(qū)分,后面文章中所提及的task皆看做宏任務(macro task)蔫磨。

macro-task(宏任務)主要包含:script(整體代碼)淘讥、setTimeout、setInterval堤如、I/O蒲列、UI交互事件窒朋、setImmediate(Node.js 環(huán)境)

micro-task(微任務)主要包含:Promise、MutaionObserver蝗岖、process.nextTick(Node.js 環(huán)境)

setTimeout/Promise等API便是任務源侥猩,而進入任務隊列的是由他們指定的具體執(zhí)行任務。來自不同任務源的任務會進入到不同的任務隊列抵赢。其中setTimeout與setInterval是同源的欺劳。
分析示例代碼

console.log('script start');
setTimeout(function() {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});
console.log('script end');

1、整體script作為第一個宏任務進入主線程铅鲤,遇到console.log划提,輸出script start
2、遇到setTimeout邢享,其回調(diào)函數(shù)被分發(fā)到宏任務Event Queue中
3鹏往、遇到Promise,其then函數(shù)被分到到微任務Event骇塘,Queue中,記為then1伊履,之后又遇到了then函數(shù),將其分到微任務Event Queue中款违,記為then2
4唐瀑、遇到console.log,輸出script end
1插爹、執(zhí)行微任務介褥,首先執(zhí)行then1,輸出promise1,然后執(zhí)行then2递惋,輸出promise2,這樣就清空了所有微任務
2溢陪、執(zhí)行setTimeout任務萍虽,輸出setTimeout 至此,輸出的順序是:script start, script end,promise1, promise2, setTimeout

再來一個例子:

setTimeout(()=>{
console.log("定時器開始執(zhí)行");
})
new Promise(function(resolve){
    console.log("準備執(zhí)行for循環(huán)了");
    for(var i=0;i<100;i++){
        i==22&amp;&amp;resolve();
    }
}).then(()=>console.log("執(zhí)行then函數(shù)"))形真;
console.log("代碼執(zhí)行完畢");

首先執(zhí)行script下的宏任務,遇到setTimeout,將其放到宏任務的【隊列】里 遇到 new

Promise直接執(zhí)行,打印"準備執(zhí)行for循環(huán)" 遇到then方法,是微任務,將其放到微任務的【隊列里】 打印 “代碼執(zhí)行完畢”

本輪宏任務執(zhí)行完畢,查看本輪的微任務,發(fā)現(xiàn)有一個then方法里的函數(shù), 打印"執(zhí)行then函數(shù)" 到此,本輪的event loop

全部完成杉编。 下一輪的循環(huán)里,先執(zhí)行一個宏任務,發(fā)現(xiàn)宏任務的【隊列】里有一個 setTimeout里的函數(shù),執(zhí)行打印"定時器開始執(zhí)行"

所以最后的執(zhí)行順序就是:【準備執(zhí)行for循環(huán)–>代碼執(zhí)行完畢–>執(zhí)行then函數(shù)–>定時器開始執(zhí)行】
再來個有難度的例子

console.log('script start');
setTimeout(function() {
  console.log('timeout1');
}, 10);
new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})
console.log('script end');

首先,事件循環(huán)從宏任務(macrotask)隊列開始咆霜,最初始邓馒,宏任務隊列中,只有一個script(整體代碼)任務蛾坯;當遇到任務源(task source)時光酣,則會先分發(fā)任務到對應的任務隊列中去。所以脉课,就和上面例子類似救军,首先遇到了console.log财异,輸出script start; 接著往下走唱遭,遇到setTimeout任務源戳寸,將其分發(fā)到任務隊列中去,記為timeout1拷泽; 接著遇到promise疫鹊,new promise中的代碼立即執(zhí)行,輸出promise1,然后執(zhí)行resolve,遇到setTimeout,將其分發(fā)到任務隊列中去司致,記為timemout2,將其then分發(fā)到微任務隊列中去拆吆,記為then1; 接著遇到console.log代碼蚌吸,直接輸出script end 接著檢查微任務隊列锈拨,發(fā)現(xiàn)有個then1微任務,執(zhí)行羹唠,輸出then1 再檢查微任務隊列奕枢,發(fā)現(xiàn)已經(jīng)清空,則開始檢查宏任務隊列佩微,執(zhí)行timeout1,輸出timeout1缝彬; 接著執(zhí)行timeout2,輸出timeout2 至此哺眯,所有的都隊列都已清空谷浅,執(zhí)行完畢。其輸出的順序依次是:script start, promise1, script end, then1, timeout1, timeout2

流程圖

在這里插入圖片描述

有個小tip:從規(guī)范來看奶卓,microtask優(yōu)先于task執(zhí)行一疯,所以如果有需要優(yōu)先執(zhí)行的邏輯,放入microtask隊列會比task更早的被執(zhí)行夺姑。

記住墩邀,JavaScript是一門單線程語言,異步操作都是放到事件循環(huán)隊列里面盏浙,等待主執(zhí)行棧來執(zhí)行的眉睹,并沒有專門的異步執(zhí)行線程。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末废膘,一起剝皮案震驚了整個濱河市竹海,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丐黄,老刑警劉巖斋配,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡许起,警方通過查閱死者的電腦和手機十偶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來园细,“玉大人惦积,你說我怎么就攤上這事∶推担” “怎么了狮崩?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鹿寻。 經(jīng)常有香客問我睦柴,道長,這世上最難降的妖魔是什么毡熏? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任坦敌,我火速辦了婚禮,結果婚禮上痢法,老公的妹妹穿的比我還像新娘狱窘。我一直安慰自己,他們只是感情好财搁,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布蘸炸。 她就那樣靜靜地躺著,像睡著了一般尖奔。 火紅的嫁衣襯著肌膚如雪搭儒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天提茁,我揣著相機與錄音淹禾,去河邊找鬼。 笑死茴扁,一個胖子當著我的面吹牛铃岔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丹弱,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼铲咨!你這毒婦竟也來了躲胳?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤纤勒,失蹤者是張志新(化名)和其女友劉穎坯苹,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摇天,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡粹湃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年恐仑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片为鳄。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡裳仆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孤钦,到底是詐尸還是另有隱情歧斟,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布偏形,位于F島的核電站静袖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏俊扭。R本人自食惡果不足惜队橙,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望萨惑。 院中可真熱鬧捐康,春花似錦、人聲如沸咒钟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽朱嘴。三九已至倾鲫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間萍嬉,已是汗流浹背乌昔。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留壤追,地道東北人磕道。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像行冰,于是被迫代替她去往敵國和親溺蕉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361