文章目錄
- 事件回圈
- 瀏覽器環境事件回圈
- node環境事件回圈
- 六個階段
- (1) setTimeout 和 setImmediate
- (2) process.nextTick
- 練習例子
- 總結:
事件回圈
在瀏覽器環境下我們的js有一套自己的事件回圈,同樣在node環境下也有一套類似的事件回圈,
瀏覽器環境事件回圈
首先,我們先來回顧一下在瀏覽器的事件回圈:
總結來說:
首先會運行主執行緒的同步代碼,每一行同步代碼都會被壓入執行堆疊,每一行異步代碼會壓入異步API中(如:定時器執行緒、ajax執行緒等;),在執行堆疊沒有要執行的代碼時,也就是我們當前主執行緒沒有同步代碼了,任務佇列會從我們的異步任務微任務佇列中取一個微任務放到我們的任務佇列中進行執行,將它的回呼函式進而再次放到執行堆疊中進行執行,當微任務佇列為空時,會在宏任務中取異步任務加到任務佇列,進而壓入執行堆疊,執行回呼函式,然后繼續在該宏任務中查找同步、異步任務,一次回圈,完成了一個事件回圈(事件輪詢)
瀏覽器環境下的例子:
例子:
console.log("1");
setTimeout(() => {
console.log("setTimeout");
}, 1);
new Promise((res, rej) => {
console.log("Promise");
res('PromiseRes')
}).then(val => {
console.log(val);
})
console.log("2");
分析:
首先執行堆疊找到第一行的同步代碼,直接扔到執行堆疊中執行,列印1,隨后為定時器setTimeout,為異步任務,將代碼放到異步對列中等待執行,隨后執行promise中的代碼,我們要清楚promise是同步執行,它的回呼是異步執行,所有列印Promise,將res(‘PromiseRes’)放到異步對列中等待執行,這個時候又遇到了同步代碼,列印2,當前主執行緒的同步代碼全部執行完畢,并且執行堆疊中沒有要執行的同步代碼,這個時候webApi會從異步佇列中去微任務佇列中的第一個,加入到事件佇列執行,將回傳的回呼函式壓入到執行堆疊中執行,列印PromiseRes,隨后微任務執行完畢,已經沒有微任務,現在就需要從宏任務佇列中取宏任務定時器,加入到任務佇列中,將回呼函式壓入到執行堆疊中執行,列印setTimeout,
node環境事件回圈
在node中事件回圈主要分為六個階段來實作:
外部資料輸入–》輪詢階段–》檢查階段–》關閉事件回呼階段–》定時器階段–》I/O回呼階段–》閑置階段–》輪詢階段》…開始回圈
六個階段
圖片來自網路

timers階段:用來執行timer(setTimeout,setInterval)的回呼;
I/O callbacks階段:處理一些上一輪回圈中少數未執行的I/O回呼
idle,prepare 階段:僅node內部使用,我們用不到;
poll階段:獲取新的I/O時間,適當的條件下node將阻塞在這里;
check階段:執行setImmediate()的回呼;
close callbacks 階段:執行socket的close時間回呼
主要階段:
timer:
timers階段會執行setTimeout和setInterval回呼,并且是由poll階段控制的,
同樣,在node中定時器指定的時間也不是準確時間,只能是盡快執行,
poll:
poll這一階段中,系統會做兩件事情:
1.回到timer階段執行回呼
2.執行I/O回呼
并且在進入該階段時如果沒有設定了timer 的話,會發生以下兩件事情
如果 poll 佇列不為空,會遍歷回呼佇列并同步執行,直到佇列為慷訓者達到系統限制
如果 poll 佇列為空時,會有兩件事發生
1、如果有 setImmediate 回呼需要執行,poll 階段會停止并且進入到 check 階段執行回呼
2、如果沒有 setImmediate 回呼需要執行,會等待回呼被加入到佇列中并立即執行回呼,這里同樣會有個超時時間設定防止一直等待下去
當然設定了 timer 的話且 poll 佇列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執行回呼,
check階段
setImmediate()的回呼會被加入 check 佇列中,從 event loop 的階段圖可以知道,check 階段的執行順序在 poll 階段之后,在進入check階段執勤poll會檢查有的話到check階段,沒有的換直接到timer階段,
(1) setTimeout 和 setImmediate
二者非常相似,區別主要在于呼叫時機不同,
setImmediate 設計在 poll 階段完成時執行,即 check 階段,只有在check階段才會執行;
setTimeout 設計在 poll 階段為空閑時,且設定時間到達后執行,但它在 timer 階段執行,表示當前執行緒沒有其他可執行的同步任務,才會在timer階段執行定時器,
這兩個執行的時機可前可后:
例子1:
// //異步任務中的宏任務
setTimeout(() => {
console.log('===setTimeout===');
},0);
setImmediate(() => {
console.log('===setImmediate===')
})

多次重復執行的結果會不同,有一種隨機的感覺,出現這種情況的原因主要和setTimeout的實作代碼有關,當我們不傳時間引數或者設定為0的時候,nodejs會取值為1,即1ms(在瀏覽器端可能取值會更大一下,不同瀏覽器也各不相同),所以在電腦cpu性能夠強,能夠在1ms內執行到timers phase的情況下,由于時間延遲不滿足回呼不會被執行,于是只能等到第二輪再執行,這樣setInterval就會先執行,
可能由于cpu多次執行相同任務用時會有細微差別,而且在1ms上下浮動,才會造成上面的隨機現象
一般情況下setTimeout為0時候會在setImmediate之前執行
例子2:
當我們傳入的值大于定時器timer執行的回呼時間的時候會直接導致定時器在下一次事件回圈中執行
setTimeout(() => {
console.log('===setTimeout===');
},10);
setImmediate(() => {
console.log('===setImmediate===')
})

例子3:
當我們將上述代碼放入一個i/o中就會固定先check再而timer:
const fs = require('fs');
fs.readFile("./any.js", (data) => {
setTimeout(() => {
console.log('===setTimeout===');
},10);
setImmediate(() => {
console.log('===setImmediate===')
})
});

在第一輪回圈中讀取檔案,在回呼中,會進入check階段進而執行setImmediate,隨后timer階段執行定時器,
setimmediate 與 settimeout 放入一個 I/O 回圈內呼叫,則 setImmediate 總是被優先呼叫
(2) process.nextTick
這個函式其實是獨立于 Event Loop 之外的,它有一個自己的佇列,當每個階段完成后,如果存在 nextTick 佇列,就會清空佇列中的所有回呼函式,并且優先于其他 microtask 執行,
例子1:
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
例子2:
const fs = require('fs');
fs.readFile("./any.js", (data) => {
process.nextTick(()=>console.log('process===2'))
setTimeout(() => {
console.log('===setTimeout===');
},10);
setImmediate(() => {
console.log('===setImmediate===')
})
});
process.nextTick(()=>console.log('process===1'))

練習例子
async function async1() {
console.log('2')
//會等待await執行完 但是不會向下執行 因為下面輸入微任務
await async2()
console.log('9')
}
function async2() {
console.log('3')
}
console.log('1')
setTimeout(function () {
console.log('11')
}, 0)
setTimeout(function () {
console.log('13')
}, 300)
setImmediate(() => console.log('12'));
process.nextTick(() => console.log('7'));
async1();
process.nextTick(() => console.log('8'));
new Promise(function (resolve) {
console.log('4')
resolve();
console.log('5')
}).then(function () {
console.log('10')
})
console.log('6')
分析:
上面的循序就是序號的順序;
首先列印1:
前面都是兩個函式宣告,所有直接列印1,這行同步代碼;
列印2:
列印完1后,都是異步代碼,加入異步任務佇列,直接到async1函式呼叫,在這個函式中列印2;
列印3:
async1這個函式是個async await函式,所有也是一個變相的同步操縱等待async2函式執行,async2執行后并不會直接列印9,原因await接受的是一個promise的then操作,所以后面屬于一個promise的回呼操作屬于微任務,加入微任務佇列;
列印4:
process.nextTick為微任務,所以會繼續執行promise,列印4;
列印5:
resolve()的回呼不會立即執行屬于微任務,加入微任務佇列,所以列印5;
列印6:
最后一個主執行緒的同步代碼,列印6;
列印7、8:
process.nextTick優先級高于其他定時器,所以會直接執行回呼函式列印7、8;
列印9、10:
這個時候需要執行微任務佇列中的微任務,目前有兩個9和10,按照先后循序,先列印9后列印10;
列印11、12:
setTimeout為0秒比setImmediate執行早,按照先后循序,先列印11后列印12;
列印13:
setTimeout為300ms的函式,列印13;
例子:
async function async1() {
console.log('2')
//會等待await執行完 但是不會向下執行 因為下面輸入微任務
await async2()
console.log('9')
}
function async2() {
console.log('3')
}
console.log('1')
setTimeout(function () {
console.log('11')
setTimeout(() => {
console.log('11-1');
},100);
setImmediate(() => {
console.log('11-2')
})
}, 0)
setTimeout(function () {
console.log('13')
setTimeout(() => {
console.log('15');
},10);
setImmediate(() => {
console.log('14')
})
}, 300)
setImmediate(() => console.log('12'));
process.nextTick(() => console.log('7'));
async1();
process.nextTick(() => console.log('8'));
new Promise(function (resolve) {
console.log('4')
resolve();
console.log('5')
}).then(function () {
console.log('10')
})
console.log('6')
總結:
理解不對的地方,還請各位大佬給予指正,
參考:https://www.cnblogs.com/everlose/p/12846375.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296975.html
標籤:其他
