瀏覽器和Node中的Event Loop

  • 為什么javascript是單線程的?

    • 因為javascript最初設(shè)計是運行在瀏覽器的,首先系統(tǒng)分配給瀏覽器的內(nèi)存不會很多,其次javascript運行在瀏覽器的目的是為了與用戶進行交互,假設(shè)javascript支持多線程,那么線程A要新增一個節(jié)點,線程B要刪除一個節(jié)點,瀏覽器就不知道到底要干嘛了。

    • 隨著CPU的發(fā)展,HTML5提出了web worker。允許web worker創(chuàng)建子線程,但是子線程得受控于主線程,且不能操作DOM。

  • Event loop是一個執(zhí)行模型,在不同的地方有不同的實現(xiàn)。在瀏覽器中遵循的是HTML的標準,在node中是基于libuv庫來實現(xiàn)的。

瀏覽器中的Event Loop

HTML標準這樣定義事件循環(huán):

為了協(xié)調(diào)事件(event),用戶交互(user interaction),腳本(script),渲染(rendering),網(wǎng)絡(luò)(networking)等,用戶代理(user agent)必須使用事件循環(huán)(event loops)。

有兩類事件循環(huán):一種針對瀏覽上下文(browsing context),還有一種針對worker(web worker)。

圖片來自演講《Help,I`m stuck in an event loop》

在主線程執(zhí)行的過程中會產(chǎn)生堆和棧。執(zhí)行棧中的代碼會依次去調(diào)用外部的API。這時候就會分同步代碼和異步代碼。同步方法就去執(zhí)行棧執(zhí)行,調(diào)用到異步方法的時候就會產(chǎn)生任務(wù)隊列,異步方法的回調(diào)就去排隊。執(zhí)行棧中的代碼(同步任務(wù)),總是在讀取"任務(wù)隊列"(異步任務(wù))之前執(zhí)行,在同步代碼執(zhí)行完了,空閑的時候才去看有沒有異步任務(wù)要執(zhí)行,如果這個時候任務(wù)隊列正好有排隊的任務(wù),就去執(zhí)行。

  • 任務(wù)隊列分為macro-task和micro-task:

    • macro-task:setTimeout,setInterval,setImmediate,I/O,UI rendering

    • micro-task:process.nextTick,Promise,Object.observer,MutationObserver

  • 在執(zhí)行任務(wù)隊列中的方法的時候,會先去執(zhí)行micro-task隊列,再循環(huán)執(zhí)行macro-task隊列。

舉個例子來說,在瀏覽器執(zhí)行以下代碼:

setTimeout(()=>{
 console.log(1);
})
console.log(2);
new Promise((resolve,reject)=>{
 console.log(3);
 resolve();
}).then(()=>{
 console.log(4)
});
console.log('end');

這樣一段代碼,執(zhí)行結(jié)果是:2,3,end,4,1

分析一下:

1、在遇到setTimeout的時候,這個方法會去macro-task排隊。

2、顯而易見,同步方法,輸出2。

3、輸出3在resolve()之前,不用等待resolve()執(zhí)行完,所以是同步的,輸出3;輸出4在then()中,需要等待執(zhí)行,屬于Promise,在micro-task排隊。

4、顯而易見,同步方法,輸出end。

5、自此同步代碼已經(jīng)執(zhí)行完了。

6、開始執(zhí)行micro-task隊列,所以輸出4。

9、開始執(zhí)行macro-task隊列,所以輸出1。

node中的Event Loop

nodejs的event loop分為6個階段,每個階段的作用如下:

  • timers:執(zhí)行setTimeout()setInterval()中到期的callback。

  • I/O callbacks:上一輪循環(huán)中有少數(shù)的I/O callback會被延遲到這一輪的這一階段執(zhí)行

  • idle, prepare:僅內(nèi)部使用(這個存疑,我也不是很懂內(nèi)部在干嘛)

  • poll:最為重要的階段,執(zhí)行I/O callback,這個指的就是比如去讀數(shù)據(jù)庫啊,讀文件之類的。在適當?shù)臈l件下會阻塞在這個階段(所謂適當?shù)臈l件我要去查一查,現(xiàn)在不確定具體是什么樣的條件叫適當)

  • check:執(zhí)行setImmediate的callback

  • close callbacks:執(zhí)行close事件的callback,例如socket.on("close",func)

當然還是需要注意的就是,在每一個階段執(zhí)行之前還是會先把micro-task里的任務(wù)執(zhí)行掉。

              |-----------------micro tasks
   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
|             |             <----- micro tasks
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘ 
|             |            <----- micro tasks
|             |
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
|             |                   └───────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐      
│  │        check          │
│  └──────────┬────────────┘
|             |            <----- micro tasks
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

舉個例子,在node下執(zhí)行代碼:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then((resolve,reject)=>{
 console.log('promise2')
 })
})

根據(jù)node下的Event Loop執(zhí)行順序來看,輸出結(jié)果應(yīng)該是:

timer1,timer2,promise1,promise2

分析一下:

首先,micro-task是空的,執(zhí)行timer階段,執(zhí)行setTimeout,輸出timer1,timer2。promise1的then和promise2的then去micro-task排隊。

然后,micro-task隊列里有方法,輸出promise1,promise2

再舉個例子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})

這段的輸出是:promise3,timer1,timer2,promise1,promise2

分析一下:因為在執(zhí)行timer階段之前,會先把micro-task隊列里的方法執(zhí)行一下,所以promise3雖然寫在后面了,也得先輸出來。

再再舉個栗子:

setTimeout(()=>{
 console.log('timer1');
 Promise.resolve().then(()=>{
 console.log('promise1')
 })
})
setTimeout(()=>{
 console.log('timer2');
 Promise.resolve().then(()=>{
 console.log('promise2')
 })
})
Promise.resolve().then(()=>{
 console.log('promise3')
})
process.nextTick(()=>{
 console.log('nexttick')
})

這段的輸出是:nexttick,promise3,timer1,timer2,promise1,promise2

分析一下:因為在micro-task隊列里的優(yōu)先級,process.nextTick一定比promise先執(zhí)行

需要注意的是:是異步方法的回調(diào)在任務(wù)隊列排隊。

/////setTimeout和setImmediate的執(zhí)行順序有待商榷,主要是會涉及到代碼執(zhí)行的時間,和HTML規(guī)定setTimeout默認的4ms問題,不足4ms補足4ms

參考文檔:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容