起航
以前都是在掘金上看別人的文章,好的點個贊。逛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')
})
複製程式碼
交換程式碼順序
process.nextTick(function(){
console.log('next tick')
})
setImmediate(function(){
console.log('immediate')
})
複製程式碼
我們發現程式碼輸出的結果是一樣的。那麼nextTick的執行機制實在setImmediate之前的。
4個函式的執行機制
在介紹4個函式的機制之前,我們來看一個有趣的現象
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
複製程式碼
輸出的結果時而是
setTimeout
setImmediate
複製程式碼
時而是
setImmediate
setTimeout
複製程式碼
為啥這兩個函式的執行的順序如此不固定呢?難道有隨機性存在嗎? 其實不然,我們來看一張圖:
這是整個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
複製程式碼
理由: 如圖所示
補充: 1.macrotask:script中程式碼、setTimeout、setInterval、I/O、UI render。
2.microtask: promise、Object.observe、MutationObserver,process.nextTick。