node事件循環(huán)中事件執(zhí)行的順序
事件循環(huán)
在瀏覽器環(huán)境下我們的js有一套自己的事件循環(huán),同樣在node環(huán)境下也有一套類似的事件循環(huán)。
瀏覽器環(huán)境事件循環(huán)
首先,我們先來回顧一下在瀏覽器的事件循環(huán):
總結(jié)來說:
首先會運行主線程的同步代碼,每一行同步代碼都會被壓入執(zhí)行棧,每一行異步代碼會壓入異步API中(如:定時器線程、ajax線程等;),在執(zhí)行棧沒有要執(zhí)行的代碼時,也就是我們當(dāng)前主線程沒有同步代碼了,任務(wù)隊列會從我們的異步任務(wù)微任務(wù)隊列中取一個微任務(wù)放到我們的任務(wù)隊列中進行執(zhí)行,將它的回調(diào)函數(shù)進而再次放到執(zhí)行棧中進行執(zhí)行,當(dāng)微任務(wù)隊列為空時,會在宏任務(wù)中取異步任務(wù)加到任務(wù)隊列,進而壓入執(zhí)行棧,執(zhí)行回調(diào)函數(shù),然后繼續(xù)在該宏任務(wù)中查找同步、異步任務(wù),一次循環(huán),完成了一個事件循環(huán)(事件輪詢)
瀏覽器環(huán)境下的例子:
例子:
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");
分析:
首先執(zhí)行棧找到第一行的同步代碼,直接扔到執(zhí)行棧中執(zhí)行,打印1,隨后為定時器setTimeout,為異步任務(wù),將代碼放到異步對列中等待執(zhí)行,隨后執(zhí)行promise中的代碼,我們要清楚promise是同步執(zhí)行,它的回調(diào)是異步執(zhí)行,所有打印Promise,將res(‘PromiseRes')放到異步對列中等待執(zhí)行,這個時候又遇到了同步代碼,打印2,當(dāng)前主線程的同步代碼全部執(zhí)行完畢,并且執(zhí)行棧中沒有要執(zhí)行的同步代碼,這個時候webApi會從異步隊列中去微任務(wù)隊列中的第一個,加入到事件隊列執(zhí)行,將返回的回調(diào)函數(shù)壓入到執(zhí)行棧中執(zhí)行,打印PromiseRes,隨后微任務(wù)執(zhí)行完畢,已經(jīng)沒有微任務(wù),現(xiàn)在就需要從宏任務(wù)隊列中取宏任務(wù)定時器,加入到任務(wù)隊列中,將回調(diào)函數(shù)壓入到執(zhí)行棧中執(zhí)行,打印setTimeout。
node環(huán)境事件循環(huán)
在node中事件循環(huán)主要分為六個階段來實現(xiàn):
外部數(shù)據(jù)輸入–》輪詢階段–》檢查階段–》關(guān)閉事件回調(diào)階段–》定時器階段–》I/O回調(diào)階段–》閑置階段–》輪詢階段》…開始循環(huán)
六個階段
圖片來自網(wǎng)絡(luò)
- timers階段:用來執(zhí)行timer(setTimeout,setInterval)的回調(diào);
- I/O callbacks階段:處理一些上一輪循環(huán)中少數(shù)未執(zhí)行的I/O回調(diào)
- idle,prepare 階段:僅node內(nèi)部使用,我們用不到;
- poll階段:獲取新的I/O時間,適當(dāng)?shù)臈l件下node將阻塞在這里;
- check階段:執(zhí)行setImmediate()的回調(diào);
- close callbacks 階段:執(zhí)行socket的close時間回調(diào)
主要階段:
timer:
timers階段會執(zhí)行setTimeout和setInterval回調(diào),并且是由poll階段控制的。
同樣,在node中定時器指定的時間也不是準(zhǔn)確時間,只能是盡快執(zhí)行。
poll:
poll這一階段中,系統(tǒng)會做兩件事情:
1.回到timer階段執(zhí)行回調(diào)
2.執(zhí)行I/O回調(diào)
并且在進入該階段時如果沒有設(shè)定了timer 的話,會發(fā)生以下兩件事情
如果 poll 隊列不為空,會遍歷回調(diào)隊列并同步執(zhí)行,直到隊列為空或者達(dá)到系統(tǒng)限制
如果 poll 隊列為空時,會有兩件事發(fā)生
1、如果有 setImmediate 回調(diào)需要執(zhí)行,poll 階段會停止并且進入到 check 階段執(zhí)行回調(diào)
2、如果沒有 setImmediate 回調(diào)需要執(zhí)行,會等待回調(diào)被加入到隊列中并立即執(zhí)行回調(diào),這里同樣會有個超時時間設(shè)置防止一直等待下去
當(dāng)然設(shè)定了 timer 的話且 poll 隊列為空,則會判斷是否有 timer 超時,如果有的話會回到 timer 階段執(zhí)行回調(diào)。
check階段
setImmediate()的回調(diào)會被加入 check 隊列中,從 event loop 的階段圖可以知道,check 階段的執(zhí)行順序在 poll 階段之后,在進入check階段執(zhí)勤poll會檢查有的話到check階段,沒有的換直接到timer階段。
(1) setTimeout 和 setImmediate
二者非常相似,區(qū)別主要在于調(diào)用時機不同。
setImmediate 設(shè)計在 poll 階段完成時執(zhí)行,即 check 階段,只有在check階段才會執(zhí)行;
setTimeout 設(shè)計在 poll 階段為空閑時,且設(shè)定時間到達(dá)后執(zhí)行,但它在 timer 階段執(zhí)行,表示當(dāng)前線程沒有其他可執(zhí)行的同步任務(wù),才會在timer階段執(zhí)行定時器。
這兩個執(zhí)行的時機可前可后:
例子1:
// //異步任務(wù)中的宏任務(wù) setTimeout(() => { console.log('===setTimeout==='); },0); setImmediate(() => { console.log('===setImmediate===') })
多次重復(fù)執(zhí)行的結(jié)果會不同,有一種隨機的感覺,出現(xiàn)這種情況的原因主要和setTimeout的實現(xiàn)代碼有關(guān),當(dāng)我們不傳時間參數(shù)或者設(shè)置為0的時候,nodejs會取值為1,即1ms(在瀏覽器端可能取值會更大一下,不同瀏覽器也各不相同),所以在電腦cpu性能夠強,能夠在1ms內(nèi)執(zhí)行到timers phase的情況下,由于時間延遲不滿足回調(diào)不會被執(zhí)行,于是只能等到第二輪再執(zhí)行,這樣setInterval就會先執(zhí)行。
可能由于cpu多次執(zhí)行相同任務(wù)用時會有細(xì)微差別,而且在1ms上下浮動,才會造成上面的隨機現(xiàn)象
一般情況下setTimeout為0時候會在setImmediate之前執(zhí)行
例子2:
當(dāng)我們傳入的值大于定時器timer執(zhí)行的回調(diào)時間的時候會直接導(dǎo)致定時器在下一次事件循環(huán)中執(zhí)行
setTimeout(() => { console.log('===setTimeout==='); },10); setImmediate(() => { console.log('===setImmediate===') })
例子3:
當(dāng)我們將上述代碼放入一個i/o中就會固定先check再而timer:
const fs = require('fs'); fs.readFile("./any.js", (data) => { setTimeout(() => { console.log('===setTimeout==='); },10); setImmediate(() => { console.log('===setImmediate===') }) });
在第一輪循環(huán)中讀取文件,在回調(diào)中,會進入check階段進而執(zhí)行setImmediate,隨后timer階段執(zhí)行定時器。
setimmediate 與 settimeout 放入一個 I/O 循環(huán)內(nèi)調(diào)用,則 setImmediate 總是被優(yōu)先調(diào)用
(2) process.nextTick
這個函數(shù)其實是獨立于 Event Loop 之外的,它有一個自己的隊列,當(dāng)每個階段完成后,如果存在 nextTick 隊列,就會清空隊列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行。
例子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'))
練習(xí)例子
async function async1() { console.log('2') //會等待await執(zhí)行完 但是不會向下執(zhí)行 因為下面輸入微任務(wù) 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:
前面都是兩個函數(shù)聲明,所有直接打印1,這行同步代碼;
打印2:
打印完1后,都是異步代碼,加入異步任務(wù)隊列,直接到async1函數(shù)調(diào)用,在這個函數(shù)中打印2;
打印3:
async1這個函數(shù)是個async await函數(shù),所有也是一個變相的同步操縱等待async2函數(shù)執(zhí)行,async2執(zhí)行后并不會直接打印9,原因await接受的是一個promise的then操作,所以后面屬于一個promise的回調(diào)操作屬于微任務(wù),加入微任務(wù)隊列;
打印4:
process.nextTick為微任務(wù),所以會繼續(xù)執(zhí)行promise,打印4;
打印5:
resolve()的回調(diào)不會立即執(zhí)行屬于微任務(wù),加入微任務(wù)隊列,所以打印5;
打印6:
最后一個主線程的同步代碼,打印6;
打印7、8:
process.nextTick優(yōu)先級高于其他定時器,所以會直接執(zhí)行回調(diào)函數(shù)打印7、8;
打印9、10:
這個時候需要執(zhí)行微任務(wù)隊列中的微任務(wù),目前有兩個9和10,按照先后循序,先打印9后打印10;
打印11、12:
setTimeout為0秒比setImmediate執(zhí)行早,按照先后循序,先打印11后打印12;
打印13:
setTimeout為300ms的函數(shù),打印13;
例子:
async function async1() { console.log('2') //會等待await執(zhí)行完 但是不會向下執(zhí)行 因為下面輸入微任務(wù) 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')
總結(jié):
到此這篇關(guān)于node事件循環(huán)中事件執(zhí)行的順序的文章就介紹到這了,更多相關(guān)node 事件執(zhí)行的順序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Nodejs中fs文件系統(tǒng)模塊的路徑動態(tài)拼接的問題和解決方案
在使用fs模塊操作文件時,如果提供的操作路徑是以./或../開頭的相對路徑時,很容易出現(xiàn)路徑動態(tài)拼接錯誤的問題,所以本文給大家介紹了Nodejs中fs文件系統(tǒng)模塊的路徑動態(tài)拼接的問題和解決方案,需要的朋友可以參考下2024-03-03Node.js中的http請求客戶端示例(request client)
本篇文章主要介紹了Node.js中的http請求客戶端示例(request client),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05使用Fetch API執(zhí)行GraphQL查詢和變體問題
這篇文章主要介紹了使用Fetch API執(zhí)行GraphQL查詢和變體問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04windows 下安裝nodejs 環(huán)境變量設(shè)置
windows 下安裝nodejs 了,也安裝了npm, 但是有時候切不能直接用request(‘ws’)這一類的東西.我覺得是確實環(huán)境變量或其他設(shè)置有問題,能否給個完整的設(shè)置方案:2017-02-02基于node實現(xiàn)websocket協(xié)議
這篇文章主要介紹了基于node實現(xiàn)websocket協(xié)議的相關(guān)資料,需要的朋友可以參考下2016-04-04Vue+Node服務(wù)器查詢Mongo數(shù)據(jù)庫及頁面數(shù)據(jù)傳遞操作實例分析
這篇文章主要介紹了Vue+Node服務(wù)器查詢Mongo數(shù)據(jù)庫及頁面數(shù)據(jù)傳遞操作,結(jié)合實例形式分析了node.js查詢MongoDB數(shù)據(jù)庫及vue前臺頁面渲染等相關(guān)操作技巧,需要的朋友可以參考下2019-12-12Node+Express搭建HTTPS服務(wù)的實現(xiàn)
最近開發(fā)需要搭建一個https的服務(wù),正好最近在用nodejs和express,本文章主要介紹了Node+Express搭建HTTPS服務(wù)的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-12-12