瀏覽器和 node 中的 Event Loop

前言

衆所周知,JavaScript 這門語言是單線程的。那也就是說,JavaScript 在同一時間只能作一件事,後面的事情必須等前面的事情作完以後才能獲得執行。javascript

任務隊列

JavaScript 單線程這件事乍一看好像沒毛病,代碼原本就是須要按順序執行的嘛,先來後到,後面的你就先等着。若是是計算量致使的排隊,那沒辦法,老老實實排吧。但若是是由於 I/O 很慢(好比發一個 Ajax 請求,須要 200ms 才能返回結果),那這個等待時間就沒太必要了,徹底能夠先執行後面其餘的任務,等你請求的數據回來了再執行 Ajax 後面的操做嘛。html

由此,JavaScript 中的任務分紅了兩種,第一種是同步任務,指的是在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;第二種是異步任務,指的是不進入主線程、而進入「任務隊列」的任務,只有「任務隊列」通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。java

其執行過程以下:node

  1. JavaScript 引擎運行 JavaScript 時,有一個主線程和一個任務隊列。
  2. 同步任務跑在主線程上面,異步任務扔進任務隊列中進行等待。
  3. 主線程中的任務執行完畢以後,回去看看任務隊列中有沒有異步任務到了須要觸發的時機。若是有,那就開始執行異步任務。
  4. 重複的執行主線程的任務和輪詢任務隊列。

Event Loop

這種主線程不斷地從任務隊列中讀取任務的機制稱爲 Event Loop(事件循環)。promise

在講 Event Loop 以前,咱們先來了解一下 macrotask(宏任務)和 microtask(微任務)。瀏覽器

宏任務

包括 setTimeoutsetIntervalsetImmediate (瀏覽器僅 IE10 支持)、I/OUI Renderingbash

微任務

包括 process.nextTicknode 獨有)、PromiseObject.observe(已廢棄)、MutatinObserver異步

這裏多說一句,Promise 的執行函數(也就是 new Promise(fn) 中的 fn)是同步任務。socket

瀏覽器中的 Event Loop

Event Loop 的實如今瀏覽器和 node 中是不同的,咱們先看瀏覽器。async

  1. 開始執行主線程的任務
  2. 主線程的任務執行完畢以後去檢查 microtask 隊列,將已經到了觸發時機的任務放進主線程。
  3. 主線程開始執行任務
  4. 主線程的任務執行完畢以後去檢查 macrotask 隊列,將已經到了觸發時機的任務放進主線程。
  5. 主線程開始執行任務
  6. 輪詢 microtaskmicrotask

看一下例子

好,講完了流程,來看下🌰。

console.log('script start'); // 同步任務

setTimeout(function() {
  console.log('setTimeout'); // 放入 宏任務 隊列
}, 0);

new Promise((resolve, reject) => {
  console.log('promise'); // 同步任務
  resolve();
})
  .then(function() {
    console.log('promise1'); // 放進 微任務 隊列
  })
  .then(function() {
    console.log('promise2'); // 放進 微任務 隊列
  });
console.log('script end'); // 同步任務
複製代碼

根據上面的標識,先執行同步任務,打印出 「script start」 、 「promise」 、 「script end」,而後開始檢查 microtask 隊列,打印出 「promise1」 和 「promise2」,而後去檢查 macrotask 隊列,打印出 「setTimeout」。

這裏 setTimeout 雖然它的延遲時間爲 0,但它是個宏任務,因此必須等同步任務和微任務執行完畢以後才輪到它。

在看一個🌰。

console.log('script start'); // 同步任務

async function async1() {
  await async2();
  console.log('async1 end'); // 這裏就是 then 裏面的代碼,放入 微任務 隊列
}
async function async2() {
  console.log('async2 end'); // 同步任務
}
async1();

setTimeout(function() {
  console.log('setTimeout'); // 放入 宏任務 隊列
}, 0);

new Promise((resolve) => {
  console.log('Promise'); // 同步任務
  setTimeout(() => {
    console.log('setTimeout promise'); // 放入 宏任務 隊列
    resolve();
  });
})
  .then(function() {
    console.log('promise1'); // 放入 微任務 隊列
  })
  .then(function() {
    console.log('promise2'); // 放入 微任務 隊列
  });

console.log('script end'); // 同步任務
複製代碼

這裏的 asyncawait 就是 Promise 的語法糖,要懂得轉換,其實上述 asyncawait 代碼等價於:

new Promise((resolve) => {
  new Promise((resolve) => {
    console.log('async2 end');
    resolve();
  });
  resolve();
}).then(() => {
  console.log('async1 end');
});
複製代碼

因此執行順序爲

script start
async2 end
promise
script end
async1 end
setTimeout
setTimeout promise
promise1
promise2
複製代碼

有人可能會有疑惑了,打印 「promise1」 和 「promise2」 是微任務,怎麼還晚於 setTimeout 宏任務呢?

雖然它們是微任務,可是因爲觸發它們的 resolve() 處於 setTimeout 宏任務之中,因此它們實際上是在第二輪微任務的輪詢中被觸發的。

好了,瀏覽器的 Event Loop 就說到這個,接下來說一下 nodeEvent Loop

node 中的 Event Loop

node 中的 Event Loop 就比較複雜了,英語好的能夠去看官方文檔

引用官文檔中的一張圖,瞭解一下 Event Loop 的六個階段。

每一個階段都有本身的任務隊列。

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
複製代碼

6 個階段

  • timer:執行setTimeoutsetInterval中到期的 callback。

  • pending callback:上一輪循環中少數的 callback 會放在這一階段執行。

  • idle, prepare:僅在內部使用。

  • poll:最重要的階段,執行 pending callback,在適當的狀況下回阻塞在這個階段。

  • check:執行setImmediate(setImmediate()是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate指定的回調函數)的 callback。

  • close callbacks:執行 close 事件的 callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)

咱們重點關注 timerpollcheck 這三個階段。

timer

這個階段執行該階段任務隊列中 setTimeoutsetInterval 到期的回調,這二者須要設置一個時間。按規則來講,是到了設定的時間以後就應該執行回調,但在實際狀況中,回調函數並非一到設定的時間就能獲得執行的,有可能被其餘的任務阻塞了,須要等其餘任務執行完成以後回調才能獲得執行。

好比你設定一個 100ms 以後的 setTimeout A 回調,可是在 95ms 時執行了一個其餘的 B 任務,須要耗時 10ms,那麼在時間來到 100ms 的時候,B 任務還在執行當中,那麼此時並不會當即執行 A 回調,而是會再等 5ms,等 B 回調完成以後,而後系統發現 A 回調的觸發時機已經到了,那趕忙去執行 A 回調。也就是說在這種狀況下,A 回調會在 105ms 的時間被執行。

poll

poll 階段主要有兩個事情要作:

  1. 執行 I/O 回調。
  2. 處理輪詢隊列中的任務。

當事件循環到達 poll 階段時,會有下面兩種狀況:

  1. poll 隊列不爲空,那就開始執行隊列中的任務,直到隊列爲空或者達到系統限制。
  2. poll 隊列爲空,那麼這種狀況又分兩種狀況;
    1. 若是 check 階段有 setImmediate 任務須要執行,那麼就當即結束當前階段,轉到 check 階段執行該階段隊列中的回調。
    2. 若是 check 階段沒有 setImmediate 任務須要執行,那麼此時會停留在 poll 階段進行等待,等待有任務進到任務隊列中進行執行。

在 2.2 的狀況中,還會去檢查 timer 階段有沒有任務到了執行時間,若是有,那麼轉入 timer 階段執行隊列中到期的任務。

check

此階段會執行 setImmediate 回調,一旦此階段的任務隊列中有了 setImmediate 回調任務,且 poll 階段的任務執行完了,處於空閒狀態,那麼就會當即轉到 check 階段執行此階段任務隊列中的任務。

轉入此階段的條件check 任務隊列中有了任務,poll 階段處於閒置狀態,或者 poll 階段等待超時。

setTimeout 和 setImmediate

這二者很類似,也有些不一樣。

  • setImmediate設計用於在當前poll階段完成後 check 階段執行腳本 。
  • setTimeout 安排在通過最小設定時間後運行的腳本,在timers階段執行。

大部分時間 setImmediate 會比 setTimeout 先執行,但也有例外。好比下列代碼:

setImmediate(() => {
  console.log('setImmediate');
});
setTimeout(() => {
  console.log('setTimeout');
});
複製代碼

若是這兩個任務是在 check 以後 timer 以前加入到各自階段的任務隊列中的,那麼會先執行 setTimeout,其餘狀況會先執行 setImmediate

總的來講,setImmediate 在大部分的狀況下會比 setTimeout 先執行。

process.nextTick

從技術上來講,process.nextTick 並不屬於 Event Loop 的一部分,它會在每一個階段執行完畢轉入下一個階段的以前執行。若是有多個 process.nextTick 語句(無論它們是否嵌套),都會在當前階段結束以後所有執行。

好比:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B() {
    console.log(2);
  });
});

setTimeout(() => {
  console.log('setTimeout');
}, 0);
複製代碼

這段代碼會輸出:「1 => 2 => setTimeout」

再來看下 setImmediate

setImmediate(function A() {
  console.log(1);
  setImmediate(function B() {
    console.log(2);
  });
});

setTimeout(() => {
  console.log('setTimeout');
}, 0);
複製代碼

這段代碼的老是在最後輸出 2,說明 setImmediate 會將它裏面的事件註冊到下一個循環中。

因爲 process.nextTick 裏面的 process.nextTick 也會在當前階段執行,那麼若是 process.nextTick 發生了嵌套,那麼就會產生無限循環,不再會轉入其餘階段。

process.nextTick(function foo() {
  process.nextTick(foo);
});
複製代碼

promise

node 中的 promiseprocess.nextTick 都屬於微任務,它也會在每一個階段執行完畢以後調用,可是它的優先級會比 process.nextTick 低。

參考

JavaScript 運行機制詳解:再談Event Loop

The Node.js Event Loop, Timers, and process.nextTick()

相關文章
相關標籤/搜索