天天看點

浏覽器 EventLoop 事件循環并發模型

前置概念

在了解 EventLoop 事件循環 前,先鋪墊一些基礎概念。

堆和棧

堆和棧是計算機領域的術語:

棧(stack)又名堆棧,它是一種運算受限的線性表。限定僅在表尾進行插入和删除操作的線性表。
堆(heap)是計算機科學中一類特殊的資料結構的統稱。

簡單說(不嚴謹),我們程式中執行 function 後,這些函數将以 棧幀(Stack Frame) 的形式推到棧内,這些 棧幀 将根據先進先出的方式按序執行。 執行過程中,涉及到的引用變量将放置到堆記憶體中。

程序和線程

程序和線程在計算機領域術語,簡單看下他們描述:

線程(thread):是作業系統能夠進行運算排程的最小機關。它被包含在程序之中,是程序中的實際運作機關。一條線程指的是程序中一個單一順序的控制流,一個程序中可以并發多個線程,每條線程并行執行不同的任務。
程序(Process):是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎。在早期面向程序設計的計算機結構中,程序是程式的基本執行實體;在當代面向線程設計的計算機結構中,程序是線程的容器。程式是指令、資料及其組織形式的描述,程序是程式的實體。

有一個例子比喻的很好: 程序 好比一個工廠,它有獨立資源。多個工廠之間 互相獨立 。 線程 是工廠中的勞工,多個勞工協作完成任務。 工廠内有一個或多個勞工 ,勞工之間 共享工廠内的資源 。

浏覽器是多程序的!

打開電腦任務管理器,我們能看到目前正在運作的 程式程序 :

浏覽器 EventLoop 事件循環并發模型

多個相同的程序會歸到一類中,比如我在 Chrome 浏覽器打開了多個 tab 頁面,此時總程序數達到了 35 個。

浏覽器 EventLoop 事件循環并發模型

此時會有個疑問,為何 tab 數量和浏覽器程序數量不一緻?可以打開更多工具/任務管理器檢視浏覽器運作的具體内容:

浏覽器 EventLoop 事件循環并發模型
浏覽器 EventLoop 事件循環并發模型

能看到有 GPU,浏覽器基礎功能,擴充程式程序,另外每個 tab 頁對應的程序外,其他與其相關的 Service Worker 及輔助架構線程。 詳細關于浏覽器程序的說明,可以看在浏覽器輸入 URL 後發生了什麼?js 為何是單線程的?

首先如果知道浏覽器内部的程序和線程的運作模式,那就容易了解為何 js 是單線程: 簡單說,我們打開頁面後, 浏覽器程序 就開始工作,并拉取對應前端資源。拉到資源後,移交給 渲染程序 做頁面渲染。 渲染程序 會開設一條 主線程 用來做 DOM 的解析,計算樣式,以及建構出 Layout樹,Layer樹... 雖然 主線程 已經做了那麼多工作,但它還要運作我們的 js 代碼 。因為這個 主線程 被設計為 線程 ,并且單個 渲染程序 中隻存在一個 主線程 ,是以跑在裡面的 js 也隻能是單線程級的,環境如何是以何談多線程,或者多程序。 從另一方面了解,如果同一段代碼由多個 js 線程在運作,必然存在并發問題,涉及到 DOM 的修改的話,浏覽器也不知道怎麼處理渲染關系。

EventLoop 事件循環并發模型

函數和棧的運作方式

這是 MDN 上有關 js 運作時的模型概念圖:

浏覽器 EventLoop 事件循環并發模型

它分為3個部分:存放棧幀的棧隊列(Stack),存放對象的堆記憶體(Heap),以及放置待處理的消息隊列 (Message Queue)。 了解大概組成部分後,下面通過一段代碼方法解釋棧和堆:

function foo(b) {
  let a = 10;
  return a + b + 11;
}

function bar(x) {
  let y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 傳回 42
           

MDN 對其詳細說明:

當調用 bar 時,第一個幀被建立并壓入棧中,幀中包含了 bar 的參數和局部變量。當 bar 調用 foo 時,第二個幀被建立并被壓入棧中,放在第一個幀之上,幀中包含 foo 的參數和局部變量。當 foo 執行完畢然後傳回時,第二個幀就被彈出棧(剩下 bar 函數的調用幀)。當 bar 也執行完畢然後傳回時,第一個幀也被彈出,棧就被清空了。

每調用一次方法,該方法及參數将以 棧幀 的形式推到 棧内 ,相關引用對象變量則會放到 堆記憶體 中。下圖是幀棧的形成過程:

浏覽器 EventLoop 事件循環并發模型

消息隊列和EventLoop

注意到還有個 消息隊列 Queue 沒有講到,在 script 代碼中,根據不同的 Web APIs(比如:點選事件,定時任務,網絡請求等)分為不同的任務:宏任務 macroTasks 和微任務 mircoTasks。任務會以 回調函數 Callback 按“一定的規則”放到消息隊列中。 EventLoop 是一個自始至終運作的機制,它會等待消息隊列是否還有任務加入,如果沒有,會按“一定規則”運作消息隊列中的回調函數。其僞代碼如下:

// 僞代碼
while (queue.waitForMessage()) { //等待待處理消息
  queue.processNextMessage(); //執行消息
}
           

整個 EventLoop 的運作過程如下:

浏覽器 EventLoop 事件循環并發模型

宏任務與微任務

這是一張主程序内描述了 微任務隊列 和 事件隊列 的概覽圖:

宏任務包括: script代碼,setTimeout,setImmediate,I/O 等。 微任務包括:Promise,MutationObserver,process.nextTick(nodejs)等。 相信一定看過類似的題目,試着說下列印順序?

console.log('script start');

setTimeout(function () {
  console.log('setTimeout');
}, 0);

Promise.resolve()
  .then(function () {
    console.log('promise1');
  })
  .then(function () {
    console.log('promise2');
});

console.log('script end');

// script start
// promise1,promise2
// setTimeout
// script end
           

首先 script 會以宏任務身份運作,會将調用代碼推到棧内,秉着先進先出的規則,先輸出 script start。 緊接着遇到 setTimeout 和 Promise,根據 Web APIs 分别歸類到 宏任務 Event 隊列 和 微任務的 microtask queue (PromiseJobs)隊列,因為第一個宏任務沒有結束, EventLoop 并不會開始執行消息隊列的回調函數(隻有在 JavaScript 引擎中沒有其它任務在運作時,才開始執行任務隊列中的任務),最後運作到末尾的代碼,輸出 script end,目前宏任務正式結束。 接下來 EventLoop 會從消息隊列依次把回調函數拿來執行,由于第一個 setTimeout 的定時即使為 0 但它還是沒有推到消息隊列,微任務則會優先于它,輸出了 promise1,promise2,當微任務全部執行完後,等到下次 EventlLoop 的循環才輸出了 setTimeout。

浏覽器 EventLoop 事件循環并發模型

非阻塞式I/O

知道了 EventLoop 的運作過程後,就能了解為什麼 js 是一種 非阻塞式I/O 的模型。 符合特定規則的 Web APIs 會專門放置到一個任務隊列,依靠 EventLoop 機制,在主任務代碼運作完後,不停輪詢調用這些任務隊列中的回調方法。這樣我們的 js 代碼不會是同步式,而是各種 Callback 回調風格的事件驅動式。 由于 js 依舊是單線程,如果我們主線程上的代碼邏輯比較“吃”性能,就會造成一定的卡頓。是以對于 cpu 密集的場景需要考慮技術的選型。

參考

  • 并發模型與事件循環 - JavaScript | MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
  • 事件循環:微任務和宏任務: https://zh.javascript.info/event-loop
  • 關于JavaScript單線程的一些事: https://github.com/JChehe/blog/blob/master/posts/%E5%85%B3%E4%BA%8EJavaScript%E5%8D%95%E7%BA%BF%E7%A8%8B%E7%9A%84%E4%B8%80%E4%BA%9B%E4%BA%8B.md
  • 從浏覽器多程序到JS單線程,JS運作機制最全面的一次梳理: https://segmentfault.com/a/1190000012925872
  • 這一次,徹底弄懂 JavaScript 執行機制 - 掘金: https://juejin.cn/post/6844903512845860872
  • JavaScript main thread. Dissected. : https://medium.com/@francesco_rizzi/javascript-main-thread-dissected-43c85fce7e23
  • JavaScript Event Loop And Call Stack Explained: https://felixgerschau.com/javascript-event-loop-call-stack/
  • Javascript Engine, Call Stack, Callback Queue, Web API and Event Loop: https://simonzhlx.github.io/js-engine/
  • 微任務、宏任務與Event-Loop - 掘金: https://juejin.cn/post/6844903657264136200
  • 簡述js中的同步阻塞和異步非阻塞 - 掘金: https://juejin.cn/post/6844903850483138568

繼續閱讀