這一章,咱們來學習一下event_loop, 本文內容旨在釐清瀏覽器(browsing context)和Node環境中不一樣的 Event Loop。javascript
首先清楚一點:瀏覽器環境和 node環境的event-loop
徹底不同。java
爲了協調事件、用戶交互、腳本、UI渲染、網絡請求等行爲,用戶引擎必須使用Event Loop
。event loop
包含兩類:基於browsing contexts,基於worker。node
本文討論的瀏覽器中的EL基於browsing contextspromise
上面圖中,關鍵性的兩點:瀏覽器
同步任務直接進入主執行棧(call stack)中執行bash
等待主執行棧中任務執行完畢,由EL將異步任務推入主執行棧中執行網絡
task在網上也被成爲macrotask
(宏任務)dom
script代碼異步
setTimeout/setIntervalsocket
setImmediate (未實現)
I/O
UI交互
一個event loop
中,有一個或多個 task隊列。
不一樣的task會放入不一樣的task隊列中:好比,瀏覽器會爲鼠標鍵盤事件分配一個task隊列,爲其餘的事件分配另外的隊列。
先進隊列的先被執行
微任務
一般下面幾種任務被認爲是microtask
promise(promise
的then
和catch
纔是microtask,自己其內部的代碼並非)
MutationObserver
process.nextTick(nodejs環境中)
一個EL中只有一個microtask隊列。
一個EL只要存在,就會不斷執行下邊的步驟:
先執行同步代碼,全部微任務,一個宏任務,全部微任務(,更新渲染),一個宏任務,全部微任務(,更新渲染)...... 執行完microtask隊列裏的任務,有可能會渲染更新。在一幀之內的屢次dom變更瀏覽器不會當即響應,而是會積攢變更以最高60HZ的頻率更新視圖
setTimeout(() => console.log('setTimeout1'), 0);
setTimeout(() => {
console.log('setTimeout2');
Promise.resolve().then(() => {
console.log('promise3');
Promise.resolve().then(() => {
console.log('promise4');
})
console.log(5)
})
setTimeout(() => console.log('setTimeout4'), 0);
}, 0);
setTimeout(() => console.log('setTimeout3'), 0);
Promise.resolve().then(() => {
console.log('promise1');
})
複製代碼
打印出來的結果是 :
promise1
setTimeout1
setTimeout2
'promise3'
5
promise4
setTimeout3
setTimeout4
複製代碼
另一個例子:
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')
setTimeout(() => {
console.log('sssss')
}, 0)
})
.then(function () {
console.log('promise2')
})
console.log('script end')
複製代碼
在瀏覽器內輸出結果以下, node內輸出結果不一樣
'script start'
'async2 end'
'Promise'
'script end'
'async1 end'
'promise1'
'promise2'
'setTimeout'
'sssss'
複製代碼
await 只是 fn().then()
這些寫法的語法糖,至關於 await
那一行代碼下面的代碼都被當成一個微任務,推入到了microtask queue
中
順序:執行完同步任務,執行微任務隊列中的所有的微任務,執行一個宏任務,執行所有的微任務
Node中的event-loop
由 libuv庫 實現,js是單線程的,會把回調和任務交給libuv
event loop
首先會在內部維持多個事件隊列,好比 時間隊列、網絡隊列等等,而libuv會執行一個至關於 while true的無限循環,不斷的檢查各個事件隊列上面是否有須要處理的pending狀態事件,若是有則按順序去觸發隊列裏面保存的事件,同時因爲libuv的事件循環每次只會執行一個回調,從而避免了 競爭的發生
我的理解,它與瀏覽器中的輪詢機制(一個task,全部microtasks;一個task,全部microtasks…)最大的不一樣是,node輪詢有phase(階段)的概念,不一樣的任務在不一樣階段執行,進入下一階段以前執行全部的process.nextTick() 和 全部的microtasks。
timers階段
在這個階段檢查是否有超時的timer(setTimeout/setInterval),有的話就執行他們的回調
但timer設定的閾值不是執行回調的確切時間(只是最短的間隔時間),node內核調度機制和其餘的回調函數會推遲它的執行
由poll階段來控制何時執行timers callbacks
複製代碼
I/O callback 階段
處理異步事件的回調,好比網絡I/O,好比文件讀取I/O,當這些事件報錯的時候,會在 `I/O` callback階段執行
複製代碼
poll 階段
這裏是最重要的階段,poll階段主要的兩個功能:
處理poll queue的callbacks
回到timers phase執行timers callbacks(當到達timers指定的時間時)
進入poll階段,timer的設定有下面兩種狀況:
1. event loop進入了poll階段, **未設定timer**
poll queue不爲空:event loop將同步的執行queue裏的callback,直到清空或執行的callback到達系統上限
poll queue爲空
若是有設定` callback`, event loop將結束poll階段進入check階段,並執行check queue (check queue是 setImmediate設定的)
若是代碼沒有設定setImmediate() callback,event loop將阻塞在該階段等待callbacks加入poll queue
2. event loop進入了 poll階段, **設定了timer**
若是poll進入空閒狀態,event loop將檢查timers,若是有1個或多個timers時間時間已經到達,event loop將回到 timers 階段執行timers queue
這裏的邏輯比較複雜,流程能夠藉助下面的圖進行理解:
![](https://ws1.sinaimg.cn/large/006tKfTcgy1g0anodoa11j311i0h0t8w.jpg)
複製代碼
check 階段
一旦poll隊列閒置下來或者是代碼被`setImmediate`調度,EL會立刻進入check phase
複製代碼
close callbacks
關閉I/O的動做,好比文件描述符的關閉,鏈接斷開等
若是socket忽然中斷,close事件會在這個階段被觸發
複製代碼
同步的任務執行完,先執行徹底部的process.nextTick()
和 所有的微任務隊列,而後執行每個階段,每一個階段執行完畢後,
調用階段不同
不一樣的io中,執行順序不保證
兩者很是類似,區別主要在於調用時機不一樣。
setImmediate
設計在poll階段完成時執行,即check段;
setTimeout
設計在poll階段爲空閒時,且設定時間到達後執行,但它在timer階段執行
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
複製代碼
對於以上代碼來講,setTimeout 可能執行在前,也可能執行在後。 首先 setTimeout(fn, 0) === setTimeout(fn, 1)
,這是由源碼決定的。
若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調。 若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了。
也就是說,進入事件循環也是須要成本的。有可能進入event loop 時,setTimeout(fn, 1)
還在等待timer中,並無被推入到 time 事件隊列
,而setImmediate
方法已經被推入到了 check事件隊列
中了。那麼event_loop 按照time
、i/o
、poll
、check
、close
順序執行,先執行immediate
任務。
也有可能,進入event loop 時,setTimeout(fn, 1)
已經結束了等待,被推到了time
階段的隊列中,以下圖所示,則先執行了timeout
方法。
因此,setTimeout
setImmediate
哪一個先執行,這主要取決於,進入event loop 花了多長時間。
但當兩者在異步i/o callback內部調用時,老是先執行setImmediate,再執行setTimeout
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
複製代碼
在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。
官方推薦使用
setImmediate()
,由於更容易推理,也兼容更多的環境,例如瀏覽器環境
process.nextTick()
在當前循環階段結束以前觸發
setImmediate()
在下一個事件循環中的check階段觸發
經過process.nextTick()
觸發的回調也會在進入下一階段前被執行結束,這會容許用戶遞歸調用 process.nextTick()
形成I/O被榨乾,使EL不能進入poll階段
所以node做者推薦咱們儘可能使用setImmediate,由於它只在check階段執行,不至於致使其餘異步回調沒法被執行到
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
複製代碼
注意:主棧執行完了以後,會先清空 process.nextick() 隊列和microtask隊列中的任務,而後按照每個階段來執行先處理異步事件的回調,好比網絡I/O,好比文件讀取I/O。當這些I/O動做都結束的時候,在這個階段會觸發它們的
另一個例子
const {readFile} = require('fs')
setTimeout(() => {
console.log('1')
}, 0)
setTimeout(() => {
console.log('2')
}, 100)
setTimeout(() => {
console.log('3')
}, 200)
readFile('./test.js', () => {
console.log('4')
})
readFile(__filename, () => {
console.log('5')
})
setImmediate(() => {
console.log('當即回調')
})
process.nextTick(() => {
console.log('process.nexttick的回調')
})
Promise.resolve().then(() => {
process.nextTick(() => {
console.log('nexttick 第二次回調')
})
console.log('6')
}).then(() => {
console.log('7')
})
複製代碼
上面代碼的結果是:
process.nexttick的回調
6
7
nexttick 第二次回調
1
當即回調
4
5
2
3
複製代碼
上面代碼須要注意點:
下面兩個回調任務,要等100ms
和 200ms
才能被推入到timers
階段的任務隊列
兩個讀取文件的回調,須要等待讀取完成後,才能被推入到 poll
階段的任務隊列。(不是被推入到 io
階段的任務隊列,只有讀取失敗等異常的回調,纔會被推入到 io
階段的任務隊列)
在微任務裏面,新添加的process.nextTick()
也會在新階段的開始以前被執行。簡單理解爲,在每個階段的任務隊列開始以前,都須要所有清空process.nextTick
和 microtask
任務隊列
本身在驗證上面的想法的時候,實驗過不少代碼,從未失手過,可是當實驗到下面的代碼時:
Promise.resolve().then(() => {
console.log(1)
Promise.resolve().then(() => {
console.log(2)
})
}).then(() => {
console.log(3)
})
複製代碼
按照上面咱們講的,這裏應該是輸出132
, 可是反覆驗證,在 node
實際輸出的是 123
,連續好幾天都不得其解,後來看到一個問答,才恍然大悟: stackoverflow.com/questions/3…
首先,上面的代碼,在.then()
的回調函數中去執行promise.resolve()
, 其實是, 在目前的promise 鏈
中新建了一個獨立的 promise鏈
。 你沒有任何辦法保證這兩個哪一個先執行完,這其實是node引擎 的一個bug,就像一口氣發出兩個請求,並不知道哪一個請求先返回。
每次咱們都能獲得相同的結果是由於,咱們Promise.resolve()
裏面剛好沒有異步的操做,這並非event-loop
專門設計成這樣的。
因此,沒必要花太多的時間,在上面的代碼中,實際寫代碼中,也不會出現這種狀況。