解讀setTimeout, promise.then, process.nextTick, setImmediate的執行順序

最近在看《Node.js調試指南》的時候遇到有意思的幾道題,是關於setTimeout, promise.then, process.nextTick, setImmediate的執行順序。今天抽空記錄下這道題的分析過程及背後的原理與知識點。
題目以下:node

// 題目一:
setTimeout(()=>{
    console.log('setTimeout')
},0)
setImmediate(()=>{
    console.log('setImmediate')
})
// 題目二:
const promise = Promise.resolve()
promise.then(()=>{
    console.log('promise')
})
process.nextTick(()=>{
    console.log('nextTick')
})
// 題目三:
setTimeout (() => { 
  console.log(1)
},0)
new Promise((resolve,reject) => { 
  console.log(2)
  for(let i = 0; i <10000; i++) {
      i === 9999 && resolve()
  }         
  console.log(3) 
}).then(() => { 
  console.log(4)
})
console.log(5)
// 題目四
setInterval(()=>{
    console.log('setInterval')
},100)
process.nextTick(function tick(){
    process.nextTick(tick)
})

在分析這幾道題以前先有必要了解下node.js的事件循環編程

事件循環 Event Loop

咱們能夠簡單理解Event Loop以下:promise

  1. 全部任務都在主線程上執行,造成一個執行棧(Execution Context Stack)
  2. 在主線程以外還存在一個任務隊列(Task Queen),系統把異步任務放到任務隊列中,而後主線程繼續執行後續的任務
  3. 一旦執行棧中全部的任務執行完畢,系統就會讀取任務隊列。若是這時異步任務已結束等待狀態,就會從任務隊列進入執行棧,恢復執行
  4. 主線程不斷重複上面的第三步
    上面第三步中的讀取任務隊列包括如下6個階段
  • timers:執行setTimeout()和setInterval()中到期的callback
  • I/O callbacks:上一輪循環中有少數的I/O callback會被延遲到這一輪的這一階段
  • idle,prepare:僅內部調用
  • poll:最重要的階段,執行I/O callback,在某些條件下node會阻塞在這個階段
  • check:執行setImmediate()的callback
  • close callbacks:執行close事件的callback,例如socket.on('close',func)

每一個階段都有一個FIFO的回調隊列,當Event Loop執行到這個階段時,就會從當前階段的隊列裏拿出一個任務放到執行棧中執行,在隊列任務清空或者執行的回調數量達到上限後,Event Loop就會進入下一個階段異步

poll階段

poll階段主要有兩個功能,以下所述:socket

  1. 當timers的定時器到期後,執行定時器(setTimeout和setInterval)的callback
  2. 執行poll隊列裏面的I/O callback
    若是Event Loop進入了poll階段,且代碼未設定timer,則可能發生如下的狀況:
  • 若是poll queue不爲空,則Event Loop將同步執行queue裏的callback,直至queue爲空,或者執行的callback達到系統上限
  • 若是poll queue爲空,則可能發生如下狀況:
    • 若是代碼中使用了setImmediate(),則Event Loop將結束poll階段並進入check階段,執行check階段的代碼
    • 若是代碼中沒有使用setImmediate(),則Event Loop將阻塞在該階段,等待callback加入poll queue,若是有callback進來則當即執行

一旦poll queue爲空,則Event Loop將檢查timers,若是有timer的時間到期,則Event Loop將回到timers階段,而後執行timer queue函數

事件循環原理

  1. node 的初始化
    1. 初始化 node 環境。
    2. 執行輸入代碼。
    3. 執行 process.nextTick 回調。
    4. 執行 microtasks。
  2. 進入 event-loop
    1. 進入 timers 階段
      • 檢查 timer 隊列是否有到期的 timer 回調,若是有,將到期的 timer 回調按照 timerId 升序執行。
      • 檢查是否有 process.nextTick 任務,若是有,所有執行。
      • 檢查是否有microtask,若是有,所有執行。
      • 退出該階段。
    2. 進入IO callbacks階段。
      • 檢查是否有 pending 的 I/O 回調。若是有,執行回調。若是沒有,退出該階段。
      • 檢查是否有 process.nextTick 任務,若是有,所有執行。
      • 檢查是否有microtask,若是有,所有執行。
      • 退出該階段。
    3. 進入 idle,prepare 階段:
      • 這兩個階段與咱們編程關係不大,暫且按下不表。
    4. 進入 poll 階段
      • 首先檢查是否存在還沒有完成的回調,若是存在,那麼分兩種狀況。
        • 第一種狀況:
          • 若是有可用回調(可用回調包含到期的定時器還有一些IO事件等),執行全部可用回調。
          • 檢查是否有 process.nextTick 回調,若是有,所有執行。
          • 檢查是否有 microtaks,若是有,所有執行。
          • 退出該階段。
        • 第二種狀況:
          • 若是沒有可用回調。
          • 檢查是否有 immediate 回調,若是有,退出 poll 階段。若是沒有,阻塞在此階段,等待新的事件通知。
      • 若是不存在還沒有完成的回調,退出poll階段。
    5. 進入 check 階段。oop

      • 若是有immediate回調,則執行全部immediate回調。
      • 檢查是否有 process.nextTick 回調,若是有,所有執行。
      • 檢查是否有 microtaks,若是有,所有執行。
      • 退出 check 階段
    6. 進入 closing 階段。線程

      • 若是有immediate回調,則執行全部immediate回調。
      • 檢查是否有 process.nextTick 回調,若是有,所有執行。
      • 檢查是否有 microtaks,若是有,所有執行。
      • 退出 closing 階段
    7. 檢查是否有活躍的 handles(定時器、IO等事件句柄)。調試

      • 若是有,繼續下一輪循環。
      • 若是沒有,結束事件循環,退出程序。

經過上面的事件循環的介紹咱們已經知道setTimeout setImmediate的執行機制,可是並無介紹process.nextTick()和promise.then()。這裏咱們還須要知道宏任務與微任務的概念code

宏任務 Macrotask

宏任務是指Event Loop在每一個階段執行的任務
宏任務包括 script (總體代碼),setTimeout, setInterval, setImmediate, I/O, UI renderin

微任務 Microtask

微任務是指Event Loop在每一個階段之間執行的任務
微任務包括 process.nextTick, Promise.then,Object.observe,MutationObserver

宏任務與微任務執行順序圖

圖中綠色小塊表示Event Loop的各個階段,執行的是宏任務,粉色箭頭表示執行的是微任務

瞭解到這裏咱們再來分析上面的幾道題
題目一的執行結果是:

setTimeout
setImmediate
//或者
setImmediate
setTimeout

爲何結果不肯定呢?咱們知道setTimeout的回調函數在timer階段執行,setImmediate的回調函數在check階段執行。可是從事件循環開始到timer階段會消耗必定的時間,因此會出現兩種狀況:

  1. 若timer前的準備時間超過1ms,則執行timer階段(setTimeout)的回調函數
  2. 若timer前的準備時間少於1ms,則執行check階段(setImmediate)的回調函數,下次event loop循環在執行timer階段的函數

題目二的執行結果是

nextTick
promise

這裏雖然和process.nextTick同樣,promise.then也將回調函數註冊到microtask,但process.nextTick的microtask queue老是優先於promise的microtask queue執行的
題目三的執行結果是

2
3
5
4
1

Promise構造函數是同步執行的,因此先打印2,3,在打印5,接下來事件循環執行微任務執行promise.then的回調,打印4,而後進入下一個事件循環執行timer階段的回調打印1

題目四的執行結果是
永遠不會打印setInterval

process.nextTick會無限循環,將event loop阻塞在microtask階段,致使event loop上其餘macrotask階段的回調函數沒有機會執行
解決方法一般是用setImmediate代替process.nextTick.
在setImmediate內執行setImmedaite時會將immediate註冊到下一次event loop的check階段,這樣其餘macrotask就有機會執行

至此終於將node.js事件循環宏任務與微任務分析清楚了

相關文章
相關標籤/搜索