SetTimeout、SetInterVal、setImmediate和process.nextTick的理解

綠箭字益達發表於2019-03-04

起航

以前都是在掘金上看別人的文章,好的點個贊。逛github的時候,也只是喜歡看別人原始碼,也從不在git上上傳東西。今天突然萌發了自己想要發一些東西,方便自己以後查閱,也能讓大家指點一下,是否正確。免得在錯誤的道路上越走越遠,還和正確的人爭論的面紅耳赤。畢竟一旦確立了錯誤的觀點,那麼錯誤的觀點就會在你潛意識裡變成正確的了。

疑問

正常情況下,setTimeout、setInterval、setImmediate和process.nextTick都是非同步執行的,那麼這四個函式方法的執行機制和時間到底是如何的呢,各自有什麼區別呢?能否替換呢?這一切都要從nodejs的event loop上面出發才能 有所理解吧。

setTimeout和setInterval

先分別介紹各個函式,setTimeout和setInterval最為相似,在函式分析上,我們知道,setTimeout和setInterval的函式格式都是如下:

    setTimeout(function(arg1,arg2){
        //some code
    },XXX)
    setInterval(function(arg1,arg2){
        //some code
    },XXX)
複製程式碼

那麼這個XXX延遲時間是有個規定的,延遲時間的範圍是[1,2^31-1]。當你延遲時間設定小於1或者大於2^31-1的時候,延遲時間預設被修改成1,即當你寫setTimeout(function(arg1,arg2){},0.1)其實等價於寫了setTimeout(function(arg1,arg2){},1)。

setImmediate和nextTick

我們直接看程式碼,這兩個函式的執行結果如何:

   setImmediate(function(){
    console.log('immediate')
   })
   process.nextTick(function(){
    console.log('next tick')
   })
複製程式碼

SetTimeout、SetInterVal、setImmediate和process.nextTick的理解
交換程式碼順序

   process.nextTick(function(){
    console.log('next tick')
   })
   setImmediate(function(){
    console.log('immediate')
   })
複製程式碼

SetTimeout、SetInterVal、setImmediate和process.nextTick的理解
我們發現程式碼輸出的結果是一樣的。那麼nextTick的執行機制實在setImmediate之前的。

4個函式的執行機制

在介紹4個函式的機制之前,我們來看一個有趣的現象

setTimeout(() => {
    console.log('setTimeout')
}, 0)

setImmediate(() => {
    console.log('setImmediate')
})
複製程式碼

輸出的結果時而是

setTimeout
setImmediate
複製程式碼

時而是

setImmediate
setTimeout
複製程式碼

為啥這兩個函式的執行的順序如此不固定呢?難道有隨機性存在嗎? 其實不然,我們來看一張圖:

SetTimeout、SetInterVal、setImmediate和process.nextTick的理解
這是整個event loop的簡略圖,很多東西我都刪減掉了,I/O裡面的細節操作我逗縮寫在一個步驟裡了。 我們用通俗距離方法來說吧,setTimeout和setInterval的等級是一樣的,所以方法在程式碼裡按照先後順序註冊執行。但是按上面程式碼輸出,為什麼1和3的步驟會出現隨機性輸出呢?

setTimeout的回撥函式在1階段執行,setImmediate的回撥函式在3階段執行。event loop先檢測1階段,這個是正確的,官方文件也說了The event loop cycle is timers -> I/O -> immediates, rinse and repeat. 但是有個問題就是進入第一個event loop時間不確定,不一定就是從頭開始進 入的,上面的例子進入的時間並不完整。網上有人總結,當進入event loop的 時間低於1ms,則進入check階段,也就是3階段,呼叫setImmediate,如果超過1ms,則進入的是timer階段,也就是1階段,回撥setTimeout的回撥函式。

所以4個函式的機制我們可以總結了:在1階段(timer階段),我們註冊的是setTimeout和setInterval回撥函式,在I/O階段之後的3階段(check階段),我們註冊的是setImmediate的回撥函式。現在就剩下process.nextTick函式了。這個函式比較特殊,他註冊時間實在上圖中綠色箭頭的tick階段。

用些題目來加深下理解(這些題目都是從網上copy而來)

題目一

const fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout')
  }, 0)

  setImmediate(() => {
    console.log('setImmediate')
  })
})
複製程式碼

執行結果:

setImmediate 
setTimeout
複製程式碼

理由:

timer -- I/O -- check。這三個階段是event loop的執行順序,當fs讀取檔案時,我們已經將setTimeout和setImmediate註冊在event loop中了,當fs檔案流讀取完畢,執行到了I/O階段,然後去執行check階段,執行setImmediate的回撥函式,然後去下一次輪詢的時候進入到timer階段執行setTimeout。

題目二

setInterval(() => {
  console.log('setInterval')
}, 100)

process.nextTick(function tick () {
  process.nextTick(tick)
})
複製程式碼

執行結果:

無任何輸出,setInterval永遠不執行
複製程式碼

理由:

因為process.nextTick是註冊在tick階段的,回撥的仍然是process.nextTick方法,但是process.nextTick不是註冊在下一個輪詢的tick階段,而是在當前的tick階段進行拼接,繼續執行,從而導致了死迴圈,event loop根本沒機會進入到timer階段

###題目三

setImmediate(() => {  ------ 1
  console.log('setImmediate1')
  setImmediate(() => {  ------2
    console.log('setImmediate2')
  })
  process.nextTick(() => { -------3
    console.log('nextTick')
  })
})

setImmediate(() => { ------4
  console.log('setImmediate3')
})

複製程式碼

執行結果:

setImmediate1
setImmediate3
nextTick
setImmediate2
複製程式碼

理由:

先將最外層第一個setImmediate,即標號為1註冊,然後註冊最外層標號為2的setImmediate。接下來註冊第一個setImmediate裡面的非同步函式。先註冊標號為3的setImmediate的函式,然後註冊標號為4的process.nextTick。此時進入event loop執行回撥,先執行1裡面的函式,輸出setImmediate1,由於3和4都在2之後註冊的,此時執行的是標號為4的回撥方法,輸出setImmediate3。繼續輪詢,由於process.nextTick是註冊在4之後的tick中,所以先執行process.nextTick,最好輪詢執行2的回撥方法,輸出setImmediate2

題目四

const promise = Promise.resolve()

promise.then(() => {
  console.log('promise')
})

process.nextTick(() => {
  console.log('nextTick')
})
複製程式碼

輸出結果:

nextTick
promise
複製程式碼

理由:

promise.then也是註冊在tick階段的,但是process.nextTick的優先順序高於promise,故而先呼叫process.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)
複製程式碼

輸出結果:

2
3
5
4
1
複製程式碼

理由:

new promise是個同步操作,故而輸出2和3,然後執行最後一行程式碼輸出5。接下來就是promise.then和setTimeout的問題了。我們知道promise.then和process.nextTick一樣是註冊在tick階段的,而setTimeout是註冊在timer階段的,先進入tick階段執行,然後在進入到下一個輪詢的setTimeout。

題目六

setImmediate(() => {
    console.log(1)
    setTimeout(() => {
        console.log(2)
    }, 100)
    setImmediate(() => {
      console.log(3)
    })
    process.nextTick(() => {
      console.log(4)
    })
  })
  setImmediate(() => {
    console.log(5)
    setTimeout(() => {
      console.log(6)
    }, 100)
    setImmediate(() => {
      console.log(7)
    })
    process.nextTick(() => {
      console.log(8)
    })
  })
複製程式碼

輸出結果

1
5
4
8
3
7
2
6
複製程式碼

理由:

這裡的tick會合並,所以4和8連續輸出

題目七

setImmediate(() => { ---1
  console.log(1)
  setTimeout(() => {   ---2
    console.log(2)
  }, 100)
  setImmediate(() => { ---3
    console.log(3)
  })
  process.nextTick(() => { ---4
    console.log(4)
  })
})
process.nextTick(() => { ---5
  console.log(5)
  setTimeout(() => { ---6
    console.log(6)
  }, 100)
  setImmediate(() => { ---7
    console.log(7)
  })
  process.nextTick(() => { ---8
    console.log(8)
  })
})
console.log(9)
複製程式碼

輸出結果

9
5
8
1
7
4
3
6
2
複製程式碼

理由: 如圖所示

SetTimeout、SetInterVal、setImmediate和process.nextTick的理解

補充: 1.macrotask:script中程式碼、setTimeout、setInterval、I/O、UI render。

2.microtask: promise、Object.observe、MutationObserver,process.nextTick。

相關文章