我是這樣理解EventLoop的
一、前言
??眾所周知,在使用javascript時(shí),經(jīng)常需要考慮程序中存在異步的情況,如果對異步考慮不周,很容易在開發(fā)中出現(xiàn)技術(shù)錯(cuò)誤和業(yè)務(wù)錯(cuò)誤。作為一名合格的javascript使用者,了解異步的存在和運(yùn)行機(jī)制十分重要且有必要;那么,異步究竟是何方神圣呢?我們不得不提Event Loop:也叫做事件循環(huán),是指瀏覽器或Node環(huán)境的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種機(jī)制,也就是實(shí)現(xiàn)異步的原理。作為一種單線程語言,javascript本身是沒有異步這一說法的,是由其宿主環(huán)境提供的(EventLoop優(yōu)秀文章網(wǎng)上有很多,這篇文章是自己的整合和理解)。
注意:Event Loop 并不是在 ECMAScript 標(biāo)準(zhǔn)中定義的,而是在 HTML 標(biāo)準(zhǔn)中定義的;
二、Event Loop知識鋪墊
??javascript代碼運(yùn)行時(shí),任務(wù)被分為兩種,宏任務(wù)(MacroTask/Task)和微任務(wù)(MircoTask);Event Loop在執(zhí)行和協(xié)調(diào)各種任務(wù)時(shí)也將任務(wù)隊(duì)列分為Task Queue和MircoTak Queue分別對應(yīng)管理宏任務(wù)(MacroTask/Task)和微任務(wù)(MircoTask);作為隊(duì)列,Task Queue和MircoTak Queue也具備隊(duì)列特性:先進(jìn)先出(FIFO—first in first out)。
1、微任務(wù)(MircoTask)
??在 HTML 標(biāo)準(zhǔn)中,并沒有明確規(guī)定 Microtask,但是實(shí)際開發(fā)中包含以下四種:
- Promise中的
then、catch、finally(原理參考:【js進(jìn)階】手撕Promise,一碼一解析 包懂) - MutationObserver(監(jiān)視 DOM 變動(dòng)的API,詳情參考MDN)
Object.observe(廢棄:監(jiān)聽標(biāo)準(zhǔn)對象的變化)- Process.nextTick(Node環(huán)境,通常也被認(rèn)為是微任務(wù))
2、宏任務(wù)(MacroTask/Task)
??基本上,我們將javascript中非微任務(wù)(MircoTask)的所有任務(wù)都?xì)w為宏任務(wù),比如:
- script中全部代碼
- DOM操作
- 用戶交互操作
- 所有的網(wǎng)路請求
- 定時(shí)器相關(guān)的 setTimeout、setInterval 等
- ···
3、javascript runtime
??javascript runtime:為 JavaScript 提供一些對象或機(jī)制,使它能夠與外界交互,是javascript的執(zhí)行環(huán)境。javascript執(zhí)行時(shí)會(huì)創(chuàng)建一個(gè)main thread主線程和call-stack 調(diào)用棧(執(zhí)行棧,遵循后進(jìn)先出的規(guī)則),所有的任務(wù)都會(huì)被放到調(diào)用棧/執(zhí)行棧等待主線程執(zhí)行。其運(yùn)行機(jī)制如下:
- 1)主線程自上而下依次執(zhí)行所有代碼;
- 2)同步任務(wù)直接進(jìn)入到主線程被執(zhí)行;
- 3)異步任務(wù)進(jìn)入到
Event Table,當(dāng)異步任務(wù)有結(jié)果后,將相對應(yīng)的回調(diào)函數(shù)進(jìn)行注冊,放入Event Queue; - 4)主線程任務(wù)執(zhí)行完空閑下來后,從
Event Queue(FIFO)中讀取任務(wù),放入主線程執(zhí)行; - 5)放入主線程的
Event Queue任務(wù)繼續(xù)從第一步開始,如此循環(huán)執(zhí)行;
上述步驟執(zhí)行過程就是我們所說的事件循環(huán)(Event Loop),上圖展示了事件循環(huán)中的一個(gè)完整循環(huán)過程。
三、瀏覽器環(huán)境的Event Loop
??不同的執(zhí)行環(huán)境中,Event Loop的執(zhí)行機(jī)制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 實(shí)現(xiàn)并提供了 ECMAScript 標(biāo)準(zhǔn)中的所有數(shù)據(jù)類型、操作符、對象和方法(注意并沒有 DOM)。但它們的 Runtime 并不一樣:Chrome 提供了 window、DOM,而 Node.js 則是 require、process 等等。我們在了解瀏覽器中Event Loop的具體表現(xiàn)前需要先整理同步、異步、微任務(wù)、宏任務(wù)之間的關(guān)系!
1、同步、異步 和 宏任務(wù)、微任務(wù)
??看到這里,可能會(huì)有很多疑惑:同步異步很好理解,宏任務(wù)微任務(wù)上面也進(jìn)行了分類,但是當(dāng)他們四個(gè)在一起后就感覺很混亂了,冥冥之中覺得同步異步和宏任務(wù)微任務(wù)有內(nèi)在聯(lián)系,但是他們之間有聯(lián)系嗎?又是什么聯(lián)系呢?網(wǎng)上有的文章說宏任務(wù)就是同步的,微任務(wù)就是異步的 這種說法明顯是錯(cuò)的!
??其實(shí)我更愿意如此描述:宏任務(wù)和微任務(wù)是相對而言的,根據(jù)代碼執(zhí)時(shí)循環(huán)的先后,將代碼執(zhí)行分層理解,在每一層(一次)的事件循環(huán)中,首先整體代碼塊看作一個(gè)宏任務(wù),宏任務(wù)中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是該宏任務(wù)層的微任務(wù);宏任務(wù)中的同步代碼進(jìn)入主線程中立即執(zhí)行的,宏任務(wù)中的非微任務(wù)異步執(zhí)行代碼將作為下一次循環(huán)的宏任務(wù)時(shí)進(jìn)入調(diào)用棧等待執(zhí)行的;此時(shí),調(diào)用棧中等待執(zhí)行的隊(duì)列分為兩種,優(yōu)先級較高先執(zhí)行的本層循環(huán)微任務(wù)隊(duì)列(MicroTask Queue),和優(yōu)先級低的下層循環(huán)執(zhí)行的宏任務(wù)隊(duì)列(MacroTask Queue)!
注意:每一次/層循環(huán),都是首先從宏任務(wù)開始,微任務(wù)結(jié)束;
2、簡單實(shí)例分析
上面的描敘相對拗口,結(jié)合代碼和圖片分析理解:
??答案暫時(shí)不給出,我們先進(jìn)行代碼分析:這是一個(gè)簡單而典型的雙層循環(huán)的事件循環(huán)執(zhí)行案例,在這個(gè)循環(huán)中可以按照以下步驟進(jìn)行分析:
- 1、首先區(qū)分出該層
宏任務(wù)的范圍(整個(gè)代碼); - 2、區(qū)分
宏任務(wù)中同步代碼和異步代碼
同步代碼:console.log('script start');、console.log('enter promise');和console.log('script end');;
異步代碼塊:setTimeout和Promise的then(注意:Promise中只有then、catch、finally的執(zhí)行需要等到結(jié)果,Promise傳入的回調(diào)函數(shù)屬于同步執(zhí)行代碼); - 3、在
異步中找出同層的微任務(wù)(代碼中的Promise的then)和下層事件循環(huán)的宏任務(wù)(代碼中的setTimeout) - 4、
宏任務(wù)的同步代碼優(yōu)先進(jìn)入主線程,按照自上而下順序執(zhí)行完畢;
輸出順序?yàn)椋?/li>
//同步代碼執(zhí)行輸出
script start
enter promise
script end
- 5、當(dāng)主線程空閑時(shí),執(zhí)行該層的
微任務(wù)
//同層微任務(wù)隊(duì)列代碼執(zhí)行輸出
promise then 1
promise then 2
- 6、首層事件循環(huán)結(jié)束,進(jìn)入第二層事件循環(huán)(
setTimeout包含的執(zhí)行代碼,只有一個(gè)同步代碼)
//第二層宏任務(wù)隊(duì)列代碼執(zhí)行輸出
setTimeout
綜合分析最終得出數(shù)據(jù)結(jié)果為:
//首層宏任務(wù)代碼執(zhí)行輸出
script start
enter promise
script end
//首層微任務(wù)隊(duì)列代碼執(zhí)行輸出
promise then 1
promise then 2
//第二層宏任務(wù)隊(duì)列代碼執(zhí)行輸出
setTimeout
3、復(fù)雜案例分析
??那么,你是否已經(jīng)了解上述執(zhí)行過程了呢?如果完全理解上述實(shí)例,說明你已經(jīng)大概知道瀏覽器中Event Loop的執(zhí)行機(jī)制,但是,要想知道自己是不是完全明白,不妨對于下列多循環(huán)的事件循環(huán)進(jìn)行分析檢驗(yàn),給出你的結(jié)果:
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4')
})
setTimeout(function() {
console.log('5');
new Promise(function(resolve) {
console.log('6');
resolve();
}).then(function() {
console.log('7')
})
})
console.log('14');
})
new Promise(function(resolve) {
console.log('8');
resolve();
}).then(function() {
console.log('9')
})
setTimeout(function() {
console.log('10');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
console.log('13')
分析:如下圖草稿所示,左上角標(biāo)a為宏任務(wù)隊(duì)列,左上角標(biāo)i為微任務(wù)隊(duì)列,同一層循環(huán)中,本層宏任務(wù)先執(zhí)行,再執(zhí)行微任務(wù);本層宏任務(wù)中的非微任務(wù)異步代碼塊作為下層循環(huán)的宏任務(wù)進(jìn)入下次循環(huán),如此循環(huán)執(zhí)行;
如果你的與下面的結(jié)果一致,恭喜你瀏覽器環(huán)境的Event Loop你已經(jīng)完全掌握,那么請開始下面的學(xué)習(xí):
1->8->13->9->2->3->14->4->10->11->12->5->6->7
四、Node 環(huán)境下的 Event Loop
??在Node環(huán)境下,瀏覽器的EventLoop機(jī)制并不適用,切記不能混為一談。這里借用網(wǎng)上很多博客上的一句總結(jié)(其實(shí)我也是真不太懂):Node中的Event Loop是基于libuv實(shí)現(xiàn)的:libuv是 Node 的新跨平臺抽象層,libuv使用異步,事件驅(qū)動(dòng)的編程方式,核心是提供i/o的事件循環(huán)和異步回調(diào)。libuv的API包含有時(shí)間,非阻塞的網(wǎng)絡(luò),異步文件操作,子進(jìn)程等等。
1、Event Loop的6階段
??Node的Event loop一共分為6個(gè)階段,每個(gè)細(xì)節(jié)具體如下:
-
timers:執(zhí)行setTimeout和setInterval中到期的callback。 -
pending callback:上一輪循環(huán)中少數(shù)的callback會(huì)放在這一階段執(zhí)行。 -
idle, prepare:僅在內(nèi)部使用。 -
poll:最重要的階段,執(zhí)行pending callback,在適當(dāng)?shù)那闆r下回阻塞在這個(gè)階段。 -
check:執(zhí)行setImmediate的callback。 -
close callbacks:執(zhí)行close事件的callback,例如socket.on('close'[,fn])或者h(yuǎn)ttp.server.on('close, fn)。
注意:上面六個(gè)階段都不包括process.nextTick()
在這里插入圖片描述
重點(diǎn):如上圖所,在Node.js中,一次宏任務(wù)可以認(rèn)為是包含上述6個(gè)階段、微任務(wù)microtask會(huì)在事件循環(huán)的各個(gè)階段之間執(zhí)行,也就是一個(gè)階段執(zhí)行完畢,就會(huì)去執(zhí)行microtask隊(duì)列的任務(wù)。
2、process.nextTick()
??在第二節(jié)中就了解到,process.nextTick()屬于微任務(wù),但是這里需要重點(diǎn)提及下:
-
process.nextTick()雖然它是異步API的一部分,但未在圖中顯示。因?yàn)?code>process.nextTick()從技術(shù)上講,它不是事件循環(huán)的一部分; - 當(dāng)每個(gè)階段完成后,如果存在 nextTick,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行(
可以理解為微任務(wù)中優(yōu)先級最高的)
3、實(shí)例分析
??老規(guī)矩,線上代碼:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
console.log('13')
將代碼的執(zhí)行分區(qū)進(jìn)行解釋
分析:如下圖草稿所示,
左上角標(biāo)a為宏任務(wù)隊(duì)列,左上角標(biāo)i為微任務(wù)隊(duì)列,左上角標(biāo)t為timers階段隊(duì)列,左上角標(biāo)p為nextTick隊(duì)列同一層循環(huán)中,本層宏任務(wù)先執(zhí)行,再執(zhí)行微任務(wù);本層宏任務(wù)中的非微任務(wù)異步代碼塊作為下層循環(huán)的宏任務(wù)進(jìn)入下次循環(huán),如此循環(huán)執(zhí)行:- 1、
整體代碼可以看做宏任務(wù),同步代碼直接進(jìn)入主線程執(zhí)行,輸出1,7,13,接著執(zhí)行同層微任務(wù)且nextTick優(yōu)先執(zhí)行輸出6,8; - 2、二層中宏任務(wù)中只存在
setTimeout,兩個(gè)setTimeout代碼塊依次進(jìn)入6階段中的timer階段以t1、t2進(jìn)入隊(duì)列;代碼等價(jià)于:
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
- 3、
setTimeout中的同步代碼立即執(zhí)行輸出2,4,9,11,nextTick和Pormise.then進(jìn)入微任務(wù)執(zhí)行輸出3,10,5,12; - 4、二層中不存在
6階段中的其他階段,循環(huán)完畢,最終輸出結(jié)果為:1->7->13->6->8->2->4->9->11->3->10->5->12;
4、當(dāng)堂小考
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
setTimeout(function() {
console.log('6');
process.nextTick(function() {
console.log('7');
})
new Promise(function(resolve) {
console.log('8');
resolve();
}).then(function() {
console.log('9')
})
})
})
})
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
setTimeout(function() {
console.log('13');
process.nextTick(function() {
console.log('14');
})
new Promise(function(resolve) {
console.log('15');
resolve();
}).then(function() {
console.log('16')
})
})
})
setTimeout(function() {
console.log('17');
process.nextTick(function() {
console.log('18');
})
new Promise(function(resolve) {
console.log('19');
resolve();
}).then(function() {
console.log('20')
})
})
console.log('21')
五、總結(jié)
??瀏覽器和Node環(huán)境下,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同:Node 端,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行;瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行;