在 Node 11 版本中,Node 的 Event Loop 已經與 瀏覽器趨於相同。html
Event Loop也是js老生常談的一個話題了。2月底看了阮一峯老師的《Node定時器詳解》一文後,發現沒法徹底對標以前看過的js事件循環執行機制,又查閱了一些其餘資料,記爲筆記,感受不妥,總結成文。html5
瀏覽器中與node中事件循環與執行機制不一樣,不可混爲一談。 瀏覽器的Event loop是在HTML5中定義的規範,而node中則由libuv庫實現。同時閱讀《深刻淺出nodeJs》一書時發現比較當時node機制已有不一樣,因此本文node部分針對爲此文發佈時版本。強烈推薦讀下參考連接中的前三篇。node
js執行爲單線程(不考慮web worker),全部代碼皆在主線程調用棧完成執行。當主線程任務清空後纔會去輪詢取任務隊列中任務。git
異步任務分爲task(宏任務,也可稱爲macroTask)和microtask(微任務)兩類。 當知足執行條件時,task和microtask會被放入各自的隊列中等待放入主線程執行,咱們把這兩個隊列稱爲Task Queue(也叫Macrotask Queue)和Microtask Queue。github
即爲同步完成,一個宏任務,全部微任務,一個宏任務,全部微任務......web
new Promise((resolve, reject) =>{console.log(‘同步’);resolve()}).then(() => {console.log('異步')})
,即promise
的then
和catch
纔是microtask,自己的內部代碼不是。while (true) {
宏任務隊列.shift()
微任務隊列所有任務()
}
複製代碼
js執行爲單線程,全部代碼皆在主線程調用棧完成執行。當主線程任務清空後纔會去輪詢取任務隊列中任務。api
在node中事件每一輪循環按照順序分爲6個階段,來自libuv的實現:promise
除上述循環階段中的任務類型,咱們還剩下瀏覽器和node共有的microtask和node獨有的process.nextTick
,咱們稱之爲Microtask Queue和NextTick Queue。瀏覽器
咱們把循環中的幾個階段的執行隊列也分別稱爲Timers Queue、I/O Queue、Check Queue、Close Queue。bash
在進入第一次循環以前,會先進行以下操做:
process.nextTick()
按照咱們的循環的6個階段依次執行,每次拿出當前階段中的所有任務執行,清空NextTick Queue,清空Microtask Queue。再執行下一階段,所有6個階段執行完畢後,進入下輪循環。即:
能夠看出,nextTick
優先級比promise
等microtask高。setTimeout
和setInterval
優先級比setImmediate
高。
setImmediate
則會在此輪循環的check階段執行,若是在timers階段建立了setTimeout
,因爲timers已取出完畢,則會進入下輪循環,check階段建立timers任務同理。setTimeout
優先級比setImmediate
高,可是因爲setTimeout(fn,0)
的真正延遲不可能徹底爲0秒,可能出現先建立的setTimeout(fn,0)
而比setImmediate
的回調後執行的狀況。while (true) {
loop.forEach((階段) => {
階段所有任務()
nextTick所有任務()
microTask所有任務()
})
loop = loop.next
}
複製代碼
function sleep(time) {
let startTime = new Date()
while (new Date() - startTime < time) {}
console.log('1s over')
}
setTimeout(() => {
console.log('setTimeout - 1')
setTimeout(() => {
console.log('setTimeout - 1 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then - then')
})
})
sleep(1000)
})
setTimeout(() => {
console.log('setTimeout - 2')
setTimeout(() => {
console.log('setTimeout - 2 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then - then')
})
})
sleep(1000)
})
複製代碼
setTimeout - 1 //1爲單個task
1s over
setTimeout - 1 - then
setTimeout - 1 - then - then
setTimeout - 2 //2爲單個task
1s over
setTimeout - 2 - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over
複製代碼
setTimeout - 1
1s over
setTimeout - 2 //一、2爲單階段task
1s over
setTimeout - 1 - then
setTimeout - 2 - then
setTimeout - 1 - then - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over
複製代碼
由此也可看出事件循環在瀏覽器和node中的不一樣。
因爲新版 node 執行狀況與瀏覽器相同,因此瀏覽器環境爲例,以 console 輸出值代指值所在函數,執行過程以下
<!--執行完主執行線程中的任務。-->
<!--取出Microtask Queue中任務執行直到清空。-->
<!--取出Macrotask Queue中一個任務執行。-->
<!--取出Microtask Queue中任務執行直到清空。-->
<!--重複3和4。-->
以 IQ 代指微任務隊列,AQ 代指宏任務隊列
1. 執行完主線程中任務:主執行線程執行完畢,setTimeout-一、setTimeout-2 進入等待
2. 清空 IQ:此時 IQ 中無任務
2. 執行 AQ 中一個任務: setTimeout-1 到時間後進入 AQ 中,被執行,執行過程當中 setTimeout-1-1 進入等待狀態,setTimeout-1-then 直接進入 IQ 隊列,因爲 setTimeout-1 中有 1s 等待,此時 setTimeout-2 確定已經進入 AQ,setTimeout-1-1 也隨後進入 AQ,此時結束狀態爲 IQ: [setTimeout-1-then],AQ: [setTimeout-2, setTimeout-1-1]
3. 清空 IQ: 此時 IQ 中有 setTimeout-1-then,執行 setTimeout-1-then,執行過程當中,setTimout-1-then-then 直接被加入 IQ,因此 IQ 沒清空,因此繼續執行 setTimout-1-then-then,IQ 被清空,此時結束狀態爲 IQ: [], AQ: [setTimeout-2, setTimeout-1-1]
4. 執行 AQ 中一個任務:即執行 setTimeout-2
5. 清空 IQ: 這一步與 3 類似,因此輸出 setTimeout-2-then、setTimeout-2-then-then,IQ 清空,此時結束狀態爲 IQ: [], AQ: [setTimeout-1-1, setTimeout-2-1]
6. 執行 AQ 中一個任務:即 setTimeout-1-1
7. 清空 IQ: 自己就爲空
8. 執行 AQ 中一個任務:即 setTimeout-2-1
複製代碼