Node和瀏覽器之事件循環/任務隊列/異步順序/數據結構

前言

本文內容比較長,請見諒。若有評議,還請評論區指點,謝謝你們!

>> 目錄

  1. 開門見山:Node和瀏覽器的異步執行順序問題
  2. 兩種環境下的宏任務和微任務(macrotask && microtask)
  3. Node和瀏覽器的事件循環模型在實現層面的區別
  4. Node和瀏覽器的事件循環的任務隊列(task queue)
  5. Node和瀏覽器的事件循環模型在表現層面的差別
  6. 理清libuv的「7隊列」和Node「6隊列」的關係
  7. Node和瀏覽器環境下setTimeout的最小延遲時間
  8. setTimeout和setImmediate的執行順序詳解
  9. Node相關組成結構中涉及的數據結構

一.開門見山:Node和瀏覽器的異步執行順序問題

>> Node端的異步執行順序

Node端的異步執行順序以下
同步代碼 > process.nextTick > Promise.then中的函數 > setTimeOut(0) 或 setImmediate
  • 「備註1」 Promise中的函數,不管是resolve前的仍是後的,都屬於「同步代碼」的範圍,並非「異步代碼」
  • 「備註2」 setTimeOut(0) 或 setImmediate的執行順序取決於具體狀況,並無肯定的前後區分

>> Node端異步邏輯順序實驗論證

setTimeout (function () {
  console.log ('setTimeout');
}, 0);
setImmediate (function () {
  console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
  resolve ();
}).then (function () {
  console.log ('promise.then');
});
process.nextTick (function () {
  console.log ('next nick');
});
console.log ('同步代碼');

輸出javascript

 
備註1: Promise接收的函數的同步問題(實驗論證)
console.log ('我是同步代碼');
new Promise (function (resolve, reject) {
  console.log ('resolve前');
  resolve ();
  console.log ('resolve後');
}).then (function () {});
console.log ('我是同步代碼');

 

 
備註2: setTimeOut(0) 或 setImmediate的執行順序問題
這個問題比較複雜,可參考下面這篇文章
 

>> 瀏覽器的異步執行順序問題

瀏覽器中,涉及的異步API有:Promise, setTomeOut,setImmediate
(其中setImmediate能夠忽略不計,由於它只在egde和IE11才支持,沒錯,Chrome和火狐都是不支持的,因此固然也不建議使用)
執行順序
Promise.then中的函數 > setTimeOut(0) 或 setImmediate
如下代碼
setTimeout (function () {
  console.log ('setTimeout');
}, 0);
setImmediate (function () {
  console.log ('setImmediate');
});
new Promise (function (resolve, reject) {
  resolve ();
}).then (function () {
  console.log ('promise');
});
 
在edge瀏覽器中的測試結果爲
 

>> 參考資料

二.兩種環境下的宏任務和微任務陣營(macrotask && microtask)

咱們上面講述了不一樣的程序,它們的異步執行順序的區別,其中咱們發現,有的異步API執行快,而有的異步API執行慢,實際上,它們做爲異步任務,被分紅了宏任務和微任務兩大陣營,同時總體表現出微任務執行快於宏任務的現象html

在宏任務和微任務方面,Node和瀏覽器也是差別很大的,這是由於它們的底層實現不同。具體原理會在下面講解,下面先概述下兩種環境下的task的差異

>> 瀏覽器端的宏任務和微任務

下面簡單介紹下宏任務和微任務的陣營
  • 宏任務(macrotasks):setTimeout, setInterval, I/O,setImmediate(若是存在),requestAnimationFrame(存在爭議)
  • 微任務 (microtasks) : process.nextTick, Promises,MutationObserver

 

>> 備註解釋

  • 備註1:MutationObserver是HTML5新增的用來檢測DOM變化的,參考資料前端

  • 備註2: 部分資料認爲,requestAnimationFrame也屬於宏任務,理由是:requestAnimationFrame在MDN的定義爲,下次頁面重繪前所執行的操做,而重繪也是做爲宏任務的一個步驟來存在的,且該步驟晚於微任務的執行,參考資料java

>> Node端的宏任務和微任務

(⚠️該概念定義可能存在爭議,部分資料對Node中也作了宏任務和微任務的劃分,而部分資料則只提出了微任務的概念,而沒有涉及宏任務,本文聽從前者)
  • 微任務:process.nextTick,promise.then
  • 宏任務:setTimeout, setInterval,setImmediate
固然了,直接說宏任務的執行比微任務的解釋也許太粗糙了,沒辦法解釋不少具體的問題,好比:具體不一樣的宏任務之間的順序問題,因此,要作進一步的判斷,咱們就要理解JS事件循環中的執行階段,和隊列相關的知識
 

三.Node和瀏覽器的事件循環模型在實現層面的區別

瀏覽器的事件循環是在 HTML5 中定義的規範,而 Node 中則是由 libuv 庫實現,這是它們在實現上的根本差異。也就是說,不少時候,他們的行爲看起來很像,但event loop的內在實現卻存在差異。node

>> 瀏覽器的event loop

咱們看下規範的定義,如下援引自HTML5規範草案
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop.
「爲了協調事件,用戶交互,腳本,渲染,網絡等,用戶代理(瀏覽器)必須使用本節中描述的事件循環。每一個代理都有一個關聯的事件循環。」
也就是說,瀏覽器根據這個草案的規定,實現了事件循環,目的是用來協調瀏覽器的事件,交互和渲染的。
 

>> Node的event loop

Node的事件循環基於libuv實現,libuv是Node.js的底層依賴,一個跨平臺的異步IO庫。分別經過windows平臺下的IOCP和Unix 環境下的 libev實現跨平臺的兼容。
實際上,雖然libuv做爲Node的底層模塊,一開始是爲了Node而設計的,可是它被抽象了出來,而且不只僅爲Node服務,也服務於其餘語言,例如,它也支持了julia等語言的實現(Julia 是一個面向科學計算的語言)
 

四.Node和瀏覽器的事件循環的任務隊列

>> 參考資料

>> Node的任務隊列

Node的任務隊列總共6個:包括4個主隊列(main queue)和兩個中間隊列(intermediate queue)windows

  • 四個主隊列由libuv提供
  • 兩個中間隊列由Node.js實現
(⚠️上面這個論斷我是根據相關資料推斷的,若有不當請指正)

>> 6個隊列具體內容

  • 主隊列(main queue):包括計時器隊列,IO事件隊列,即時隊列,關閉事件處理程序隊列
  • 中間隊列(intermediate queue):包括(1)Next Ticks隊列和(2)其餘微任務隊列
(此概念 由Deepal Jayasekara,一位德國Node開發者提出,即上面文章的做者)

>> 四個主隊列

Q1.計時器隊列 (timer queue)
在計數器隊列中,Node會在這裏保存setTimeOut和setInterval添加的處理程序,因此處理到這個隊列的時候,Node會在一堆計時器中檢查有沒有過時的計時器,若是過時了,就調用其這個計時器的回調函數。若是有多個計時器到期(設置了相同的到期時間),那麼會根據設置的前後,按照順序去執行它們。
從這裏也能夠看出,爲何咱們總會強調setTimeOut和setInterval的時間偏差。這是由於只有在該循環流程中,檢查到「過時」了,纔會對計時器進行處理
 
Q2.IO事件隊列(IO events queue)
IO通常指的是和CPU之外的外部設備通訊的工做,例如文件操做和TCP/UDP網絡操做等。
Node依賴於底層模塊libuv提供的異步IO的功能。在IO事件隊列中,Node將處理全部待處理的I/O操做
 
Q3.即時隊列 (immediate queue)
處理這個隊列的時候,setImmediate設置的函數回調,會被依次調用
 
Q4.關閉事件處理程序(close handlers queue)
當處理到這個隊列的時候,Node將會處理全部I / O事件處理程序
 

>> 兩個中間隊列

Q5.next ticks隊列
保存process.nextTick調用造成的任務
 
Q6.其餘微任務隊列
保存Promise造成的任務
 

>> 主隊列和中間隊列的關係

在一輪循環中,4個主隊列,每處理完一個主隊列,接着就要把兩個中間隊列處理一次, 個人理解是:一趟循環走下來, 4個主隊列都各自被處理了一次,而2箇中間隊列則是被處理了4次。
 
圖示以下
 
這個圖可能說的不是很清楚,因此我整理了一下,以下所示:
(備註⚠️:此圖只適用於Node11.0.0版本之前的狀況! 對於Node11之後的隊列執行流程,請參考下面一節)
 

>> 瀏覽器的任務隊列

瀏覽器中只分兩種隊列:
  • 宏任務隊列(macro task)
  • 微任務隊列。(micro task)
他們的處理順序是
  1. 每次從宏任務隊列中取一個宏任務執行, 完成後, 把微任務隊列中的全部微任務,一次性處理完
  2. 不斷重複上述過程
以下圖所示
 

五.Node和瀏覽器的事件循環模型在表現層面的差別

Node和瀏覽器的區別狀況是:
  • 在Node11.0.0之前的版本,Node和瀏覽器的異步流程存在一些細節上的差別,
  • 但在Node11.0.0之後,這一差別被抹去了,由於Node主動修改了實現以和瀏覽器保持一致
吐槽:聽話的Node.js
修改先後區別在於
  • 在瀏覽器和Node11之後,每執行完一個timer類回調,例如setTimeout,setImmediate 以後,都會把微任務給執行掉(promise等)。
  • 原來Node10和之前: 當一個任務隊列(例如timer queue)裏面的回調都批量執行完了,纔去執行微任務
咱們能夠看出,微任務的執行變得更迅速了,再也不是跟在任務隊列處理完後處理,而是在單個timer類回調(setTimeout,setImmediate)處理完後,也會被處理了。
 
讓咱們分析下面這段代碼
setTimeout (function () {
  console.log ('timeout1:宏任務');
  new Promise (function (resolve, reject) {
    resolve ();
  }).then (() => {
    console.log ('promise:微任務');
  });
});
setTimeout (function () {
  console.log ('timeout2:宏任務');
});

對這段代碼
  • 若是是11之後的Node和瀏覽器:執行完第一個setTimeout後,接下來輪到Promise這類微任務執行了,因此接下來應該是輸出「promise:微任務」
  • 若是是version11之前的Node,則執行完第一個setTimeout後,由於timer隊列沒處理完,因此接下來執行的是第二個setTimeout,輸出的是「timeout2:宏任務」
運行結果
瀏覽器
Node10.16.3(nvm切換node版本)
Node11.0.0(nvm切換node版本)
咱們不難發現其中差異,Node10.16.3的表現是和瀏覽器不同的,而到了Node11,則Node和瀏覽器相一致了。
 

>> 參考資料

 

六.理清libuv的「七隊列」和Node「四個主隊列」的關係

(⚠️下面的是我的理解,若有您有更合理的觀點,請在評論區給出,謝謝)
好吧,其實上面的內容已經有點複雜了! 但是這個時候,又有個神奇的概念過來插一腳 它就是,Node官方文檔裏面提出的「七隊列」
下面介紹一下這位小夥伴

>> 咱們首先要明白的是三點

  1. 這裏的七隊列是libuv內部的概念
  2. 以前介紹的"Node六隊列"和"四個主隊列"是Node內部,但在libuv外部的實現和概念
  3. 這二者之間存在對應關係,雖然不是一一對應(下面會細講對應關係)

>> libuv七隊列圖解

 

>> 七隊列的具體做用api

  • timers:執行知足條件的 setTimeout 、setInterval 回調;promise

  • pending callbacks: 檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎全部狀況下,除了關閉的回調函數,它們由計時器和 setImmediate() 排定的以外),其他狀況 node 將在此處阻塞。瀏覽器

  • idle:僅僅供給Node系統內部使用
  • prepare:僅僅供給Node系統內部使用
  • poll:檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎全部狀況下,除了關閉的回調函數,它們由計時器和 setImmediate() 排定的以外),其他狀況 node 將在此處阻塞。
  • check:執行 setImmediate 的回調;
  • close callbacks:關閉全部的 closing handles ,一些 onclose 事件;

>> libuv七隊列和Node四個主隊列的對應關係

 

>> 參考資料

七.Node和瀏覽器環境下setTimeout的最小延遲時間

>> 瀏覽器端的最小延遲時間

「HTML5 規範規定最小延遲時間不能小於 4ms,即 x 若是小於 4,會被當作 4 來處理。 不過不一樣瀏覽器的實現不同,好比,Chrome 能夠設置 1ms,IE11/Edge 是 4ms。」

>> Node端的最小延遲時間

一句話足以:Node端沒有最小延遲時間

>> 我以爲裏面有一句話說的特別好

Node沒有最小延遲,這其實是瀏覽器和節點之間的兼容性問題。計時器(setTimeout和setImmediate)在JavaScript中是徹底未指定的(這是DOM規範,在Node中沒有用,況且瀏覽器也沒有遵循),而node實現它們的緣由僅僅是由於它們在JavaScript的歷史上很是地基礎
It doesn't have a minimum delay and this is actually a compatibility issue between browsers and node. Timers are completely unspecified in JavaScript (it's a DOM specification which has no use in Node and isn't even followed by browsers anyway) and node implements them simply due to how fundamental they've been in JavaScript's history
 

八.setTimeout(0 delay)和setImmediate的執行順序詳解

這個問題其實比較複雜,不能一律而論。

>> 總結來講

  • 在主線程中直接調用setTimeOut(0,function) 和setImmediate不能肯定其執行的前後順序
  • 可是若是在同一個IO循環中,例如在一個異步回調中調用這兩個方法,setImmediate會首先被調用

>> 具體解釋

第一.在主線程中運行如下腳本,咱們不能肯定timeout和immediate輸出的前後順序,結果受到進程性能的影響 (例子源於Node官方文檔,連接在下面給出)

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});
結果
輸出結果沒法肯定
 
第二.若是在一個IO循環中運行setTimeOut(0,function) 和setImmediate,那麼setImmediate 老是被優先調用
// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
 
輸出結果
immediate timeout

 

九.Node相關組成結構中涉及的數據結構

>> 介紹

  • setTimeout與setInterval: 調用這兩個函數建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中,每次tick執行時候都會從紅黑樹中迭代取出定時器對象。

  • process.nextTick: 將回調函數放入到隊列中,在下一輪Tick時取出執行,能夠達到setTimeout(fn,0)的效果,因爲不須要動用紅黑樹,效率更高時間複雜度爲O(1)。相比較之下。(紅黑樹時間複雜度O(lg(n)) )

  • setImmediate:的回調函數保存在鏈表中,每次Tick只執行鏈表中的一個回調函數。

>> 本節參考資料

  • 《深刻淺出Node.js》做者:樸靈,阿里巴巴數據平臺資深開發者,被尊爲Node.js的佈道者

相關文章
相關標籤/搜索