你不知道的 Event Loop

本文由圖雀社區(qū)認(rèn)證作者 Horace 寫作而成,點擊閱讀原文查看作者掘金鏈接,在文章尾部查看作者的博客鏈接[1],感謝作者的優(yōu)質(zhì)輸出,讓我們的技術(shù)世界變得更加美好?
筆者最近忙著做項目之類的,文章輸出遺落下了一段時間,這次我們就來聊一個面試中一個比較重要的知識點 —— Event Loop
可能有人會奇怪一個 EventLoop 還能寫出什么,且聽我慢慢來逼叨,看完這篇文章帶你搞定 Event Loop 以及它相關(guān)的一些知識點。
一、Event Loop 是什么在開始說 Event Loop 之前,我們先來認(rèn)識一下它到底是個什么東西。
In computer science, the event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. The event loop works by making a request to some internal or external "event provider" (that generally blocks the request until an event has arrived), then calls the relevant event handler ("dispatches the event"). The event loop is also sometimes referred to as the message dispatcher, message loop, message pump, or run loop.
上面這段是Wikipedia[2]對 Event Loop 的解釋,簡單的來說就是Event Loop是一個程序結(jié)構(gòu),用于等待和分派消息和事件我個人的理解是 JS 中的 Event Loop 是瀏覽器或 Node 的一種協(xié)調(diào) JavaScript 單線程運行時不會阻塞的一種機制。
為什么要學(xué) Event Loop?
可能有人會比較疑惑前端為什么要學(xué)看起來比較底層的 Event Loop,不僅僅是因為這是一道面試的常考題。
- 作為一個程序員,了解程序的運行機制是很重要的,這樣可以幫助你去輸出更優(yōu)質(zhì)的代碼。
- 前端是一個范圍很廣的領(lǐng)域,技術(shù)一直在更新迭代,掌握了底層的原理可以應(yīng)對新的技術(shù)。
- 一個優(yōu)秀的程序員要能讓寫的代碼按照自己想的去運行,如果連代碼本身的運行機制都無法掌握的話,就不用談什么掌控自己的代碼了。
上文我說了 Event Loop 是單線程阻塞問題的一種解決機制,所以在正式開始前還是要先從進程和線程的角度來聊一聊。眾所周知的一件事是,JavaScript 是一個單線程機制的語言,那我們先來看看進程和線程的定義:
定義
- 進程:進程是 CPU 資源分配的最小單位
- 線程:線程是 CPU 調(diào)度的最小單位
說實話,光從定義來看你根本感受不到進程和線程到底是什么樣的一個東西。簡單來說,進程簡單理解就是我們平常使用的程序,如 QQ,瀏覽器,網(wǎng)盤等。進程擁有自己獨立的內(nèi)存空間地址,擁有一個或多個線程,而線程就是對進程粒度的進一步劃分。
更通俗的來說,進程就像是一家工廠,多個工廠之間是獨立存在的。而線程就像是工廠中的那些工人,共享資源,完成同一個大目標(biāo)。
JS 的單線程
很多人都知道的是,JavaScript 是一門動態(tài)的解釋型的語言,具有跨平臺性。在被問到 JavaScript 為什么是一門單線程的語言,有的人可能會這么回答:“語言特性決定了 JavaScript 是一個單線程語言,JavaScript 天生是一個單線程語言”,這只不過是一層糖衣罷了。
JavaScript 從誕生起就是單線程,原因大概是不想讓瀏覽器變得太復(fù)雜,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果,對于一種網(wǎng)頁腳本語言來說,這就太復(fù)雜了。
準(zhǔn)確的來說,我認(rèn)為 JavaScript 的單線程是指 JavaScript 引擎是單線程的,JavaScript 的引擎并不是獨立運行的,跨平臺意味著 JavaScript 依賴其運行的宿主環(huán)境 --- 瀏覽器(大部分情況下是瀏覽器)。
瀏覽器需要渲染 DOM,JavaScript 可以修改 DOM 結(jié)構(gòu),JavaScript 執(zhí)行時,瀏覽器 DOM 渲染停止。如果 JavaScript 引擎線程不是單線程的,那么可以同時執(zhí)行多段 JavaScript,如果這多段 JavaScript 都操作 DOM,那么就會出現(xiàn) DOM 沖突。
舉個例子來說,在同一時刻執(zhí)行兩個 script 對同一個 DOM 元素進行操作,一個修改 DOM,一個刪除 DOM,那這樣話瀏覽器就會懵逼了,它就不知道到底該聽誰的,會有資源競爭,這也是 JavaScript 單線程的原因之一。
三、瀏覽器瀏覽器的多線程
之前說過,JavaScript 運行的宿主環(huán)境瀏覽器是多線程的。
以 Chrome 來說,我們可以通過 Chrome 的任務(wù)管理器來看看。
Chrome任務(wù)管理器
當(dāng)你打開一個 Tab 頁面的時候,就創(chuàng)建了一個進程。如果從一個頁面打開了另一個頁面,打開的頁面和當(dāng)前的頁面屬于同一站點的話,那么這個頁面會復(fù)用父頁面的渲染進程。
瀏覽器主線程常駐線程
- GUI 渲染線程
- 繪制頁面,解析 HTML、CSS,構(gòu)建 DOM 樹,布局和繪制等
- 頁面重繪和回流
- 與 JS 引擎線程互斥,也就是所謂的 JS 執(zhí)行阻塞頁面更新
- 負責(zé) JS 腳本代碼的執(zhí)行
- 負責(zé)準(zhǔn)執(zhí)行準(zhǔn)備好待執(zhí)行的事件,即定時器計數(shù)結(jié)束,或異步請求成功并正確返回的事件
- 與 GUI 渲染線程互斥,執(zhí)行時間過長將阻塞頁面的渲染
- 負責(zé)將準(zhǔn)備好的事件交給 JS 引擎線程執(zhí)行
- 多個事件加入任務(wù)隊列的時候需要排隊等待(JS 的單線程)
- 負責(zé)執(zhí)行異步的定時器類的事件,如 setTimeout、setInterval
- 定時器到時間之后把注冊的回調(diào)加到任務(wù)隊列的隊尾
- 負責(zé)執(zhí)行異步請求
- 主線程執(zhí)行代碼遇到異步請求的時候會把函數(shù)交給該線程處理,當(dāng)監(jiān)聽到狀態(tài)變更事件,如果有回調(diào)函數(shù),該線程會把回調(diào)函數(shù)加入到任務(wù)隊列的隊尾等待執(zhí)行
這里沒看懂沒關(guān)系,后面我會再說。
四、瀏覽器端的 Event Loop看到這里,總算是進入正題了,先講講瀏覽器端的 Event Loop 是什么樣的。
JS運行機制圖
上圖是一張 JS 的運行機制圖,Js 運行時大致會分為幾個部分:
- Call Stack:調(diào)用棧(執(zhí)行棧),所有同步任務(wù)在主線程上執(zhí)行,形成一個執(zhí)行棧,因為 JS 單線程的原因,所以調(diào)用棧中每次只能執(zhí)行一個任務(wù),當(dāng)遇到的同步任務(wù)執(zhí)行完之后,由任務(wù)隊列提供任務(wù)給調(diào)用棧執(zhí)行。
- Task Queue:任務(wù)隊列,存放著異步任務(wù),當(dāng)異步任務(wù)可以執(zhí)行的時候,任務(wù)隊列會通知主線程,然后該任務(wù)會進入主線程執(zhí)行。任務(wù)隊列中的都是已經(jīng)完成的異步操作,而不是說注冊一個異步任務(wù)就會被放在這個任務(wù)隊列中。
說到這里,Event Loop 也可以理解為:不斷地從任務(wù)隊列中取出任務(wù)執(zhí)行的一個過程。
同步任務(wù)和異步任務(wù)
上文已經(jīng)說過了 JavaScript 是一門單線程的語言,一次只能執(zhí)行一個任務(wù),如果所有的任務(wù)都是同步任務(wù),那么程序可能因為等待會出現(xiàn)假死狀態(tài),這對于一個用戶體驗很強的語言來說是非常不友好的。
比如說向服務(wù)端請求資源,你不可能一直不停的循環(huán)判斷有沒有拿到數(shù)據(jù),就好像你點了個外賣,點完之后就開始一直打電話問外賣有沒有送到,外賣小哥都會抄著鍋鏟來打你(狗頭)。因此,在 JavaScript 中任務(wù)有了同步任務(wù)和異步任務(wù),異步任務(wù)通過注冊回調(diào)函數(shù),等到數(shù)據(jù)來了就通知主程序。
概念
簡單的介紹一下同步任務(wù)和異步任務(wù)的概念。
- 同步任務(wù):必須等到結(jié)果來了之后才能做其他的事情,舉例來說就是你燒水的時候一直等在水壺旁邊等水燒開,期間不做其他的任何事情。
- 異步任務(wù):不需要等到結(jié)果來了才能繼續(xù)往下走,等結(jié)果期間可以做其他的事情,結(jié)果來了會收到通知。舉例來說就是你燒水的時候可以去做自己想做的事情,聽到水燒開的聲音之后再去處理。
從概念就可以看出來,異步任務(wù)從一定程度上來看比同步任務(wù)更高效一些,核心是提高了用戶體驗。
Event Loop
Event Loop 很好的調(diào)度了任務(wù)的運行,宏任務(wù)和微任務(wù)也知道了,現(xiàn)在我們就來看看它的調(diào)度運行機制。
JavaScript 的代碼執(zhí)行時,主線程會從上到下一步步的執(zhí)行代碼,同步任務(wù)會被依次加入執(zhí)行棧中先執(zhí)行,異步任務(wù)會在拿到結(jié)果的時候?qū)⒆缘幕卣{(diào)函數(shù)放入任務(wù)隊列,當(dāng)執(zhí)行棧中的沒有任務(wù)在執(zhí)行的時候,引擎會從任務(wù)隊列中讀取任務(wù)壓入執(zhí)行棧(Call Stack)中處理執(zhí)行。
宏任務(wù)和微任務(wù)
現(xiàn)在就有一個問題了,任務(wù)隊列是一個消息隊列,先進先出,那就是說,后來的事件都是被加在隊尾等到前面的事件執(zhí)行完了才會被執(zhí)行。如果在執(zhí)行的過程中突然有重要的數(shù)據(jù)需要獲取,或是說有事件突然需要處理一下,按照隊列的先進先出順序這些是無法得到及時處理的。這個時候就催生了宏任務(wù)和微任務(wù),微任務(wù)使得一些異步任務(wù)得到及時的處理。
曾經(jīng)看到的一個例子很好,宏任務(wù)和微任務(wù)形象的來說就是:你去營業(yè)廳辦一個業(yè)務(wù)會有一個排隊號碼,當(dāng)叫到你的號碼的時候你去窗口辦充值業(yè)務(wù)(宏任務(wù)執(zhí)行),在你辦理充值的時候你又想改個套餐(微任務(wù)),這個時候工作人員會直接幫你辦,不可能讓你重新排隊。
所以上文說過的異步任務(wù)又分為宏任務(wù)和微任務(wù),JS 運行時任務(wù)隊列會分為宏任務(wù)隊列和微任務(wù)隊列,分別對應(yīng)宏任務(wù)和微任務(wù)。
先介紹一下(瀏覽器環(huán)境的)宏任務(wù)和微任務(wù)大致有哪些:
- 宏任務(wù):
- script(整體的代碼)
- setTimeout
- setInterval
- I/O 操作
- UI 渲染 (對這個筆者持保留意見)
- Promise.then
- MutationObserver
事件運行順序
- 執(zhí)行同步任務(wù),同步任務(wù)不需要做特殊處理,直接執(zhí)行(下面的步驟中遇到同步任務(wù)都是一樣處理) --- 第一輪從 script開始
- 從宏任務(wù)隊列中取出隊頭任務(wù)執(zhí)行
- 如果產(chǎn)生了宏任務(wù),將宏任務(wù)放入宏任務(wù)隊列,下次輪循的時候執(zhí)行
- 如果產(chǎn)生了微任務(wù),將微任務(wù)放入微任務(wù)隊列
- 執(zhí)行完當(dāng)前宏任務(wù)之后,取出微任務(wù)隊列中的所有任務(wù)依次執(zhí)行
- 如果微任務(wù)執(zhí)行過程中產(chǎn)生了新的微任務(wù),則繼續(xù)執(zhí)行微任務(wù),直到微任務(wù)的隊列為空
- 輪循,循環(huán)以上 2 - 6
總的來說就是:同步任務(wù)/宏任務(wù) -> 執(zhí)行產(chǎn)生的所有微任務(wù)(包括微任務(wù)產(chǎn)生的微任務(wù)) -> 同步任務(wù)/宏任務(wù) -> 執(zhí)行產(chǎn)生的所有微任務(wù)(包括微任務(wù)產(chǎn)生的微任務(wù)) -> 循環(huán)......
注意:微任務(wù)隊列
舉個栗子
光說不練假把式,現(xiàn)在就來看一個例子:
舉個栗子
放圖的原因是為了讓大家在看解析之前可以先自己按照運行順序走一遍,寫好答案之后再來看解析。
解析:
(用綠色的表示同步任務(wù)和宏任務(wù),紅色表示微任務(wù))
+ console.log('script start')
+ setTimeout(function() {
+ console.log('setTimeout')
+ }, 0)
+ new Promise((resolve, reject)=>{
+ console.log("promise1")
+ resolve()
+ })
- .then(()=>{
- console.log("then11")
+ new Promise((resolve, reject)=>{
+ console.log("promise2")
+ resolve();
+ })
- .then(() => {
- console.log("then2-1")
- })
- .then(() => {
- console.log("then2-2")
- })
- })
- .then(()=>{
- console.log("then12")
- })
+ console.log('script end')
- 首先遇到 console.log(),輸出
script start - 遇到 setTimeout 產(chǎn)生宏任務(wù),注冊到宏任務(wù)隊列[setTimeout],下一輪 Event Loop 的時候在執(zhí)行
- 然后遇到 new Promise 構(gòu)造聲明(同步),log 輸出
promise1,然后 resolve - resolve 匹配到 promise1 的第一個 then,把這個 then 注冊到微任務(wù)隊列[then11]中,繼續(xù)當(dāng)前整體腳本的執(zhí)行
- 遇到最后的一個 log,輸出
script end,當(dāng)前執(zhí)行棧清空 - 從微任務(wù)隊列中取出隊頭任務(wù)'then11' 進行執(zhí)行,其中有一個 log,輸出
then11 - 往下遇到 new Promise 構(gòu)造聲明(同步),log 輸出
promise2,然后 resolve - resolve 匹配到 promise2 的第一個 then,把這個 then 注冊到微任務(wù)隊列[then2-1],當(dāng)前 then11 可執(zhí)行部分結(jié)束,然后產(chǎn)生了 promise1 的第二個 then,把這個 then 注冊到微任務(wù)隊列[then2-1, then12]
- 拿出微任務(wù)隊頭任務(wù)'then2-1' 執(zhí)行,log 輸出
then2-1,觸發(fā) promise2 的第二個 then,注冊到微任務(wù)隊列[then12, then2-2] - 拿出微任務(wù)隊頭任務(wù)'then12',log 輸出
then12 - 拿出微任務(wù)隊頭任務(wù)'then2-2',log 輸出
then2-2 - 微任務(wù)隊列執(zhí)行完畢,別忘了宏任務(wù)隊列中的 setTimeout,log 輸出
setTimeout
經(jīng)過以上一番縝(xia)密(gao)分析,希望沒有繞暈?zāi)悖詈蟮妮敵鼋Y(jié)果就是:script start -> promise1 -> script end -> then11 -> promise2 -> then2-1 -> then12 -> then2-2 -> setTimeout
宏任務(wù)?微任務(wù)?
不知道大家看了宏任務(wù)和微任務(wù)之后會不會有一個疑惑,宏任務(wù)和微任務(wù)都是異步任務(wù),微任務(wù)之前說過了是為了及時解決一些必要事件而產(chǎn)生的。
為什么要有微任務(wù)?
為什么要有微任務(wù)的原因前面已經(jīng)說了,這里就不再贅述,簡單說一下就是為了及時處理一些任務(wù),不然等到最后再執(zhí)行的時候拿到的數(shù)據(jù)可能已經(jīng)是被污染的數(shù)據(jù)達不到預(yù)期目標(biāo)了。是什么宏任務(wù)?什么是微任務(wù)?
相信大家在學(xué)習(xí) Event Loop 查找資料的時候,肯定各種資料里面都會講到宏任務(wù)和微任務(wù),但是不知道你有沒有靈魂拷問過你自己:什么是宏任務(wù)?什么是微任務(wù)?怎么區(qū)分宏任務(wù)和微任務(wù)?不能只是默許接受這個概念,在這里,我根據(jù)我的個人理解進行一番說(hu)明(che)宏任務(wù)和微任務(wù)的真面目
其實在 Chrome 的源碼中并沒有什么宏任務(wù)和微任務(wù)的代碼或是說明,在 JS 大會[3]上提到過微任務(wù)這個名詞,但是也沒有說到底什么是微任務(wù)。宏任務(wù)
文章最開始的時候說過,在 chrome 里,每個頁面都對應(yīng)一個進程。而該進程又有多個線程,比如 JS 線程、渲染線程、IO 線程、網(wǎng)絡(luò)線程、定時器線程等等,這些線程之間的通信是通過向?qū)ο蟮娜蝿?wù)隊列中添加一個任務(wù)(postTask)來實現(xiàn)的。宏任務(wù)的本質(zhì)可以認(rèn)為是多線程事件循環(huán)或消息循環(huán),也就是線程間通信的一個消息隊列。就拿 setTimeout 舉例來說,當(dāng)遇到它的時候,瀏覽器就會對 Event Loop 說:嘿,我有一個任務(wù)交給你,Event Loop 就會說:好的,我會把它加到我的 todoList 中,之后我會執(zhí)行它,它是需要調(diào)用 API 的。
宏任務(wù)的真面目是瀏覽器派發(fā),與 JS 引擎無關(guān)的,參與了 Event Loop 調(diào)度的任務(wù)
微任務(wù)
微任務(wù)是在運行宏任務(wù)/同步任務(wù)的時候產(chǎn)生的,是屬于當(dāng)前任務(wù)的,所以它不需要瀏覽器的支持,內(nèi)置在 JS 當(dāng)中,直接在 JS 的引擎中就被執(zhí)行掉了。
特殊的點
async 隱式返回 Promise 作為結(jié)果
執(zhí)行完 await 之后直接跳出 async 函數(shù),讓出執(zhí)行的所有權(quán)
當(dāng)前任務(wù)的其他代碼執(zhí)行完之后再次獲得執(zhí)行權(quán)進行執(zhí)行
立即 resolve 的 Promise 對象,是在本輪"事件循環(huán)"的結(jié)束時執(zhí)行,而不是在下一輪"事件循環(huán)"的開始時
再舉個栗子
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
按照之前的分析方法去分析之后就會得出一個結(jié)果:script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
可以看出 async1 函數(shù)獲取執(zhí)行權(quán)是作為微任務(wù)的隊尾,但是,在 Chrome73(金絲雀) 版本之后,async 的執(zhí)行優(yōu)化了,它會在 promise1 和 promise2 的輸出之前執(zhí)行。筆者大概了解了一下應(yīng)該是用 PromiseResolve 對 await 進行了優(yōu)化,減少了 Promise 的再次創(chuàng)建,有興趣的小伙伴可以看看 Chrome 的源碼。
五、Node 中的 Event LoopNode 中也有宏任務(wù)和微任務(wù),與瀏覽器中的事件循環(huán)類似。Node 與瀏覽器事件循環(huán)不同,其中有多個宏任務(wù)隊列,而瀏覽器是只有一個宏任務(wù)隊列。
Node 的架構(gòu)底層是有 libuv,它是 Node 自身的動力來源之一,通過它可以去調(diào)用一些底層操作,Node 中的 Event Loop 功能就是在 libuv 中封裝實現(xiàn)的。
宏任務(wù)和微任務(wù)
Node 中的宏任務(wù)和微任務(wù)在瀏覽器端的 JS 相比增加了一些,這里只列出瀏覽器端沒有的:
- 宏任務(wù)
- setImmediate
- process.nextTick
事件循環(huán)機制的六個階段
六個階段
Node 的事件循環(huán)分成了六個階段,每個階段對應(yīng)一個宏任務(wù)隊列,相當(dāng)于是宏任務(wù)進行了一個分類。
- timers(計時器)
執(zhí)行 setTimeout 以及 setInterval 的回調(diào) - I/O callbacks
處理網(wǎng)絡(luò)、流、TCP 的錯誤回調(diào) - idel, prepare --- 閑置階段
node 內(nèi)部使用 - poll(輪循)
執(zhí)行 poll 中的 I/O 隊列,檢查定時器是否到時間 - check(檢查)
存放 setImmediate 回調(diào) - close callbacks
關(guān)閉回調(diào),例如 sockect.on('close')
輪循順序
執(zhí)行的輪循順序 --- 每個階段都要等對應(yīng)的宏任務(wù)隊列執(zhí)行完畢才會進入到下一個階段的宏任務(wù)隊列
- timers
- I/O callbacks
- poll
- setImmediate
- close events
每兩個階段之間執(zhí)行微任務(wù)隊列
Event Loop 過程
- 執(zhí)行全局的 script 同步代碼
- 執(zhí)行微任務(wù)隊列,先執(zhí)行所有 Next Tick 隊列中的所有任務(wù),再執(zhí)行其他的微任務(wù)隊列中的所有任務(wù)
- 開始執(zhí)行宏任務(wù),共六個階段,從第一個階段開始執(zhí)行自己宏任務(wù)隊列中的所有任務(wù)(瀏覽器是從宏任務(wù)隊列中取第一個執(zhí)行!!)
- 每個階段的宏任務(wù)執(zhí)行完畢之后,開始執(zhí)行微任務(wù)
- TimersQueue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> TimersQueue ...
這里要注意的是,nextTick 事件是一個單獨的隊列,它的優(yōu)先級會高于微任務(wù),所以在當(dāng)前宏任務(wù)/同步任務(wù)執(zhí)行完成之后,會先執(zhí)行 nextTick 隊列中的所有任務(wù),再去執(zhí)行微任務(wù)隊列中的所有任務(wù)。
setTimeout 和 setImmediate
在這里要單獨說一下 setTimeout 和 setImmediate,setTimeout 定時器很熟悉,那就說說 setImmediate
setImmediate() 方法用于把一些需要長時間運行的操作放在一個回調(diào)函數(shù)里,并在瀏覽器完成其他操作(如事件和顯示更新)后立即運行回調(diào)函數(shù)。從定義來看就是為了防止一些耗時長的操作阻塞后面的操作,這也是為什么 check 階段運行順序排的比較后。
舉個栗子
我們來看這樣的一個例子:
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
這里涉及 timers 階段和 check 階段,按照上面的運行順序來說,timers 階段是在第一個執(zhí)行的,會早于 check 階段。運行這段程序可以看到如下的結(jié)果:
可是再多運行幾次,你就會看到如下的結(jié)果:
setImmediate 的輸出跑到 setTimeout 前面去了,這時候就是:小朋友你是否有很多的問號?
分析
我們來分析一下原因,timers 階段確實是在 check 階段之前,但是在 timers 階段時候,這里的 setTimeout 真的到了執(zhí)行的時間嗎?
這里就要先看看 setTiemout(fn, 0),這個語句的意思不是指不延遲的執(zhí)行,而是指在可以執(zhí)行 setTimeout 的時候就立即執(zhí)行它的回調(diào),也就是處理完當(dāng)前事件的時候立即執(zhí)行回調(diào)。
在 Node 中 setTimeout 第二個時間參數(shù)的最小值是 1ms,小于 1ms 會被初始化為 1(瀏覽器中最小值是 4ms),所以在這里 setTimeout(fn, 0) === setTimeout(fn, 1)
setTimeout 的回調(diào)函數(shù)在 timers 階段執(zhí)行,setImmediate 的回調(diào)函數(shù)在 check 階段執(zhí)行,Event Loop 的開始會先檢查 timers 階段,但是在代碼開始運行之前到 timers 階段(代碼的啟動、運行)會消耗一定的時間,所以會出現(xiàn)兩種情況:
timers 前的準(zhǔn)備時間超過 1ms,滿足 loop -> timers >= 1,setTimeout 的時鐘周期到了,則執(zhí)行 timers 階段(setTimeout)的回調(diào)函數(shù)
timers 前的準(zhǔn)備時間小于 1ms,還沒到 setTimeout 預(yù)設(shè)的時間,則先執(zhí)行 check 階段(setImmediate)的回調(diào)函數(shù),下一次 Event Loop 再進入 timers 階段執(zhí)行 timer 階段(setTimeout)的回調(diào)函數(shù)
最開始就說了,一個優(yōu)秀的程序員要讓自己的代碼按照自己想要的順序運行,下面我們就來控制一下 setTimeout 和 setImediate 的運行。
- 讓 setTimeout 先執(zhí)行
上面代碼運行順序不同無非就是因為 Node 準(zhǔn)備時間的不確定性,我們可以直接手動延長準(zhǔn)備時間?
const start = Date.now()
while (Date.now() - start < 10)
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
讓 setImmediate 先執(zhí)行
setImmediate 是在 check 階段執(zhí)行,相對于 setTimeout 來說是在 timers 階段之后,只需要想辦法把程序的運行環(huán)境控制在 timers 階段之后就可以了。讓程序至少從 I/O callbacks 階段開始 --- 可以套一層文件讀寫把把程序控制在 I/O callbacks 階段的運行環(huán)境中?
const fs = require('fs')
fs.readFile(__dirname, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
Node 11.x 的變化
timers 階段的執(zhí)行有所變化
setTimeout(() => console.log('timeout1'))
setTimeout(() => {
console.log('timeout2')
Promise.resolve().then(() => console.log('promise resolve'))
})
node 10 及之前的版本:
要考慮上一個定時器執(zhí)行完成時,下一個定時器是否到時間加入了任務(wù)隊列中,如果未到時間,先執(zhí)行其他的代碼。
比如:
timer1 執(zhí)行完之后 timer2 到了任務(wù)隊列中,順序為timer1 -> timer2 -> promise resolve
timer2 執(zhí)行完之后 timer2 還沒到任務(wù)隊列中,順序為timer1 -> promise resolve -> timer2node 11 及其之后的版本:
timeout1 -> timeout2 -> promise resolve
一旦執(zhí)行某個階段里的一個宏任務(wù)之后就立刻執(zhí)行微任務(wù)隊列,這和瀏覽器端運行是一致的。
小結(jié)
Node 和端瀏覽器端有什么不同
- 瀏覽器端的 Event Loop 和 Node.js 中的 Event Loop 是不同的,實現(xiàn)機制也不一樣
- Node.js 可以理解成有4個宏任務(wù)隊列和2個微任務(wù)隊列,但是執(zhí)行宏任務(wù)時有6個階段
- Node.js 中限制性全局 script 代碼,執(zhí)行完同步代碼后,先從微任務(wù)隊列 Next Tick Queue 中取出所有任務(wù)放入調(diào)用棧執(zhí)行,再從其他微任務(wù)隊列中取出所有任務(wù)放入調(diào)用棧中執(zhí)行,然后開始宏任務(wù)的6個階段,每個階段都將其宏任務(wù)隊列中的所有任務(wù)都取出來執(zhí)行(瀏覽器是只取第一個執(zhí)行),每個宏任務(wù)階段執(zhí)行完畢之后開始執(zhí)行微任務(wù),再開始執(zhí)行下一階段宏任務(wù),以此構(gòu)成事件循環(huán)
- 宏任務(wù)包括 ....
- 微任務(wù)包括 ....
看到這里,你應(yīng)該對瀏覽器端和 Node 端的 Event Loop 有了一定的了解,那就留一個題目。
不直接放代碼是想讓大家先自己思考然后在敲代碼運行一遍~
最后多一嘴
本文到這里算是結(jié)束了,還是那句話,做一個程序員要知其然更要知其所以然。我寫些文章也是想把知識輸出,檢驗自己是不是真的學(xué)懂了。文章中可能還存在一些沒有說清楚的地方或者是有錯的地方,歡迎直接指出~
參考資料
[1]博客鏈接: https://tearill.github.io/
[2]Wikipedia: https://en.wikipedia.org/wiki/Event_loop
[3]JS 大會: https://www.bilibili.com/video/BV1bE411B7ez?t=478
[4]github: https://github.com/tearill/Reading_Record
推薦閱讀
我的公眾號能帶來什么價值?(文末有送書規(guī)則,一定要看)
每個前端工程師都應(yīng)該了解的圖片知識(長文建議收藏)
為什么現(xiàn)在面試總是面試造火箭?
