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

起航

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

疑問

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

setTimeout和setInterval

先分別介紹各個函數,setTimeout和setInterval最爲類似,在函數分析上,咱們知道,setTimeout和setInterval的函數格式都是以下:node

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)。git

setImmediate和nextTick

咱們直接看代碼,這兩個函數的執行結果如何:github

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

交換代碼順序

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

咱們發現代碼輸出的結果是同樣的。那麼nextTick的執行機制實在setImmediate以前的。

4個函數的執行機制

在介紹4個函數的機制以前,咱們來看一個有趣的現象promise

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

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

輸出的結果時而是異步

setTimeout
setImmediate
複製代碼

時而是函數

setImmediate
setTimeout
複製代碼

爲啥這兩個函數的執行的順序如此不固定呢?難道有隨機性存在嗎? 其實否則,咱們來看一張圖:oop

這是整個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的回調函數。ui

因此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
複製代碼

理由: 如圖所示

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

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

相關文章
相關標籤/搜索