2018-03-23 星期五 農曆 二月初七戊戌年 【狗年】乙卯月 甲寅日
宜: 裁衣、經絡、伐木、開柱眼、拆卸、修造、動土、上樑、合脊、合壽木、入殮、除服、成服、移柩、破土
忌: 祭祀、嫁娶、出行、上樑、掘井
本文采用 自問自答的形式 配合程式碼(執行結果)來講解 event loop 的執行機制
執行環境 : node.js-8.10.0 WebStorm 2017.2.3
example 1
console.log(1);
console.log(2);
setTimeout(function(){
console.log(3)
})
setTimeout(function(){
console.log(4);
})
console.log(5)複製程式碼
執行:輸入結果 顯示如下
現在我們來例項分析下 這個程式碼執行結果為什麼是這樣的 :
Node是基於單執行緒的(主要主執行緒是單執行緒,用來執行同步任務。等遇到非同步任務的時候,會呼叫另外一條非同步執行緒來處理非同步任務,如:setTimeout 這些會影響主執行緒執行的,需要等待一段時間)
程式碼執行分析 如下: 主執行緒開始執行,自上往下,先是開始執行同步任務, 依次執行
console.log(1)
console.log(2)
複製程式碼
然後遇到 非同步任務
setTimeout(function(){
console.log(3)
})
setTimeout(function(){
console.log(4);
})複製程式碼
這時候 Node 會把 這些非同步任務 push 到一個 非同步執行棧 stack 裡面
然後繼續執行 主執行緒的 任務
console.log(5) 複製程式碼
等到所有的同步任務任務,這時候 Node 開始執行 非同步佇列 棧 stack 裡面的非同步任務
按照 堆 stack 的特徵 先進後出的特徵 會依次開始執行 棧裡面的非同步任務
等待他們執行完畢以後,會生成一個 巨集 任務佇列(下面會有詳細的介紹),按照執行前後順序來新增到這個佇列中,
佇列遵循先進先出的規則,開始依次執行 列隊
執行結果如下:
3
4複製程式碼
現在來更深入的瞭解下 Node.js event loop 機制
當Node.js啟動時會初始化event loop, 每一個event loop都會包含按如下順序六個迴圈階段,
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘複製程式碼
- timers 階段: 這個階段執行setTimeout(callback) and setInterval(callback)預定的callback;
- I/O callbacks 階段: 執行除了 close事件的callbacks、被timers(定時器,setTimeout、setInterval等)設定的callbacks、setImmediate()設定的callbacks之外的callbacks;
- idle, prepare 階段: 僅node內部使用;
- poll 階段: 獲取新的I/O事件, 適當的條件下node將阻塞在這裡;
- check 階段: 執行setImmediate() 設定的callbacks;
- close callbacks 階段: 比如socket.on(‘close’, callback)的callback會在這個階段執行.
每一個階段都有一個裝有callbacks的fifo queue(佇列),當event loop執行到一個指定階段時, node將執行該階段的fifo queue(佇列),當佇列callback執行完或者執行callbacks數量超過該階段的上限時, event loop會轉入下一下階段.
注意上面六個階段都不包括 process.nextTick() (稍後會在巨集任務和微任務)
example 2
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})複製程式碼
執行結果:
setImmediate
setTimeout
複製程式碼
或者:
setTimeout
setImmediate
複製程式碼
為什麼結果不確定呢?
解釋:setTimeout/setInterval 的第二個引數取值範圍是:[1, 2^31 - 1],如果超過這個範圍則會初始化為 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我們知道 setTimeout 的回撥函式在 timer 階段執行,setImmediate 的回撥函式在 check 階段執行,event loop 的開始會先檢查 timer 階段,但是在開始之前到 timer 階段會消耗一定時間,所以就會出現兩種情況:
- timer 前的準備時間超過 1ms,滿足 loop->time >= 1,則執行 timer 階段(setTimeout)的回撥函式
- timer 前的準備時間小於 1ms,則先執行 check 階段(setImmediate)的回撥函式,下一次 event loop 執行 timer 階段(setTimeout)的回撥函式
example 3
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
複製程式碼
執行結果:
setImmediate
setTimeout
複製程式碼
解釋:fs.readFile 的回撥函式執行完後:
- 註冊 setTimeout 的回撥函式到 timer 階段
- 註冊 setImmediate 的回撥函式到 check 階段
- event loop 從 pool 階段出來繼續往下一個階段執行,恰好是 check 階段,所以 setImmediate 的回撥函式先執行
- 本次 event loop 結束後,進入下一次 event loop,執行 setTimeout 的回撥函式
所以,在 I/O Callbacks 中註冊的 setTimeout 和 setImmediate,永遠都是 setImmediate 先執行。
巨集任務和微任務
這兩個概念屬於對非同步任務的分類,不同的API註冊的非同步任務會依次進入自身對應的佇列中,然後等待Event Loop將它們依次壓入執行棧中執行。
task主要包含:setTimeout
、setInterval
、setImmediate
、I/O
、UI互動事件
microtask主要包含:Promise
、process.nextTick
microtask 會優先 task 執行
一般 Event loop 會先清空 microtask 佇列裡面的任務,然後才回去 執行 task 裡面的 非同步任務
example 4
console.log(1);
console.log(2);
setImmediate(function(){
console.log(4);
})
setTimeout(function(){
console.log(3)
})
process.nextTick(function(){
console.log('process.nextTick')
})
Promise.resolve().then(function () {
console.log('Promise')
})複製程式碼
執行結果如下:
1
2
process.nextTick
Promise
3
4複製程式碼
先依次執行同步任務 console.log 輸出
1
2複製程式碼
然後開始執行非同步任務 stack 裡面的任務 先微任務在巨集任務
輸出
process.nextTick
Promise 複製程式碼
最後等待微任務執行完畢以後,在執行最後的巨集任務
3
4複製程式碼
待續。。。。。。