瀏覽器和Node中的事件循環機制

1、前言

前幾天聽公司一個公司三年的前端說「今天又學到了一個知識點-微任務、宏任務」,我問他這是什麼東西,因爲在吃飯他淺淺的說了下,當時沒太理解就私下學習整理一番,因爲談微任務、宏任務必談到事件循環,因而就有了這篇博客。javascript

在談到事件循環機制以前咱們須要知道一些基礎知識就是:前端

  • js是單線程的
  • js一開始是做爲腳本語言運行在客戶端

其實js是單線程在它做爲腳本語言操做dom的時候就決定了。那麼此時就有一個性能問題,那麼js在瀏覽器端是如何處理這個問題的呢?同時,js在後臺Node中又是如何解決的呢?這就是本篇須要介紹的事件循環機制,這裏我將分別以瀏覽器和Node兩個方面來分析。java

2、瀏覽器端

在講解事件循環以前先談談js中同步代碼、異步代碼的執行流程。node

2.一、js同步代碼執行過程

js引擎在執行經過代碼的過程當中,會安裝順序依次存儲到一個地方去,這個地方就叫作執行棧,當咱們調用一個方法的時候,js會生成一個和這個方法相對應的上下文(context)。這個執行環境中存在着這個方法的私有做用域,上層做用域的指向,方法的參數,這個做用域中定義的變量以及這個做用域的this對象。promise

function a() {
    console.log("method a execute...");
}
function b() {
    a();
}
function c() {
    b();
}
c();

以上面例子分析:js在執行的時候會有一個全局上下文,咱們這裏就稱爲GContext,下面分析步驟瀏覽器

  1. 調用c(),c入棧,此時棧中內容爲:GContext->c-contextC
  2. 接着調用b(),b入棧,此時棧中內容爲:GContext->c->contextC->b->contextB
  3. 接着調用a(),a入棧,此時棧中內容爲:GContext->c->contextC->b->contextB-c->contextC
  4. a執行完,a出棧,此時棧中內容爲:GContext->c->contextC->b->contextB
  5. b執行完,b出棧,此時棧中內容爲:GContext->c->contextC
  6. c執行完,b出棧,此時棧中內容爲:GContext
  7. 所有執行完,釋放資源

ok,上面是同步代碼的執行,上面會涉及到兩個核心概念:執行整個代碼的線程咱們稱之爲主線程,存放方法執行的地方咱們稱之爲執行棧.dom

2.二、js異步代碼執行過程

上面說完了同步過程,那這裏來談談異步的過程。js引擎在遇到一個異步事件,不會一直等待返回結果而是將它掛起。當異步任務執行完以後會將結果加入到和執行棧中不一樣的任務隊列當中,注意的是:此時放入隊列不會當即執行其回調,而是當主線程執行完執行棧中全部的任務以後再去隊列中查找是否有任務,若是有則取出排在第一位的事件而後將回調放入執行棧並執行其代碼。如此反覆就構成了事件循環。
image.png異步

這裏一樣有一個核心概念:任務隊列socket

2.三、微任務、宏任務

上面提到js執行異步方法的時候會將其返回結果放到隊列中,這是比較籠統的,具體來講,js會根據任務的類型將其放入不一樣的隊列,任務類型有兩種:微任務、宏任務。那麼其對應的哪些是微任務、哪些是宏任務呢?函數

  • 微任務:Promise、process.nextTick()、總體代碼script、Object.observer、MutationObserver
  • 宏任務:setTimeout()、setInterval()

瀏覽器在執行的時候,先從宏任務隊列中取出一個宏任務執行宏,而後在執行該宏任務下的全部的微任務,這是一個循環;而後再取出並執行下一個宏任務,再執行全部的微任務,這是第二個循環,以此類推.

注意:整個javascript代碼是第一個宏任務
const process = require('process')
setTimeout(function () {// 分發宏任務到EventQueue
    console.log("1");
}, 0);
setTimeout(() => {
    console.log("11");
}, 0);
setTimeout(() => {
    console.log("111");
}, 0);
new Promise(function (resolve) {
    console.log('2');
    resolve();
}).then(function () {// 發送微任務
    console.log('3');
});
// 輸出
2
3
1
11
111

2.四、小結

在瀏覽器端,在咱們執行一片script的時候,當遇到同步代碼,依次進入執行棧,遇到異步代碼,將其掛起,繼續執行其它方法,當異步方法執行完以後根據任務類型進入到任務隊列,在執行棧執行完,主線程空閒下來了以後會到任務隊列中取任務回調並執行。

3、Node端

我本身認爲Node的事件循環和瀏覽器端仍是有點區別的,它的事件循環依靠libuv引擎。

image.png

該圖來自官網,這裏展現了在node的事件循環的6個階段。

  • timers:該階段執行定時器的回調,如setTimeout() 和 setInterval()。
  • I/O callbacks:該階段執行除了close事件,定時器和setImmediate()的回調外的全部回調
  • idle, prepare:內部使用
  • poll:等待新的I/O事件,node在一些特殊狀況下會阻塞在這裏
  • check: setImmediate()的回調會在這個階段執行
  • close callbacks: 例如socket.on('close', ...)這種close事件的回調

對於咱們來講咱們更關注 timer、poll、check這三個階段便可。

poll 階段有兩個主要的功能:

  • 處理poll隊列(poll quenue)的事件(callback);
  • 執行timers的callback,當到達timers指定的時間時;

poll 階段的邏輯

  • 若是event loop進入了 poll階段,且代碼未設定timer,將會發生下面狀況:

    • a、若是poll queue不爲空,event loop將同步的執行queue裏的callback,直至queue爲空,或執行的callback到達系統上限;
    • b、若是poll queue爲空,將會發生下面狀況:

      * 若是代碼已經被setImmediate()設定了callback, event loop將結束poll階段進入check階段,並執行check階段的queue (check階段的queue是 setImmediate設定的)
      * 若是代碼沒有設定setImmediate(callback),event loop將阻塞在該階段等待callbacks加入poll queue;
  • 若是event loop進入了 poll階段,且代碼設定了timer:

    • 若是poll queue進入空狀態時(即poll 階段爲空閒狀態),event loop將檢查timers,
    • 若是有1個或多個timers時間時間已經到達,event loop將按循環順序進入 timers 階段,並執行timer queue

3.一、setTimeout、setImmediate

這兩個函數的功能仍是相似的,不一樣的是他們處於EventLoop的不一樣階段:timer、check。

setImmediate(()=>console.log("setInterval"));
setTimeout(() => {console.log("setTimeout")},0);

上面兩行代碼會輸出順序是什麼呢?其實兩種可能都有.
1.當setTimeout的0ms並不能作到絕對0ms,若是已通過了timer階段,那麼此時setTimeout就會在下一次循環中執行,也就是說先setInterval、再setTimeout。
2.第二種可能就是正常流程了,先timer、再check

若是上面的代碼再一個IO操做做呢?如:

require('fs').readFile(__filename,()=>{
    setImmediate(()=>console.log("setInterval"));
    setTimeout(() => {console.log("setTimeout")});
})

此時只可能出現一種狀況,先setInterval、再setTimeout,由於在io中已經執行過了timer(readFile時處於IO callback)。
下面一塊兒來看以下代碼:

setTimeout(() => {
    console.log("timer1")
    Promise.resolve().then(() => console.log("promise1"));
    process.nextTick(() => console.log("nextTick1"))
}, 0);
setTimeout(() => {
    console.log("timer2")
    Promise.resolve().then(() => console.log("promise2"));
    process.nextTick(() => console.log("nextTick2"))
}, 0);

按照個人理解,它的輸出應該是以下:先timer、而後切換階段的時候執行微任務.

// 狀況1
timer1
timer2
nextTick1
nextTick2
promise1
promise2

但是並非,它的輸出一直是:

// 狀況2
timer1
nextTick1
promise1
timer2
nextTick2
promise2

後臺晚上查資料由於Node11對EventLoop做了修改,爲了和瀏覽器兼容。因而呼我切換到10.8.0,發現上面兩種狀況都有(狀況1比例大於狀況2)。這點暫時還未查明什麼緣由。

3.二、小結

node中的6個階段每一個階段執行完都會伴隨着執行微任務,同個MicroTask隊列下process.tick()會優於Promise。

四 總結

本篇主要介紹了瀏覽器和Node對於事件循環機制實現,因爲能力水平有限,其中可能有誤之處歡迎指出。

歡迎關注公衆號:
碼農有道

相關文章
相關標籤/搜索