精讀《談談 Web Workers》

黃子毅發表於2018-10-22

1 引言

本週精讀的文章是 speedy-introduction-to-web-workers,是一篇 Web Workers 快速入門的文章,借精讀這篇文章的機會,談談對 Web Workers 的理解與運用。

2 概述

就像分工,你只負責編碼,而你的朋友負責設計,那你就可以專心把自己的事情做好,而且更快速的完成任務。

本文通過一個比方,描述了 Web Workers 的兩大特徵:

  1. 高效。
  2. 並行。

因為瀏覽器是單執行緒的,任何大量耗時的 JS 任務都會卡住介面,使瀏覽器無法響應任何操作,這樣的使用者體驗非常糟糕。Web Workers 可以將耗時任務拆解出去,降低主執行緒的壓力,避免主執行緒無響應。

但 CPU 資源是有限的,Web Workers 並不能增加總體執行效率,算上通訊的損耗,整體計算效率會有一定的下降。

建立 Web Workers

const worker = new Worker("../src/worker.js");
複製程式碼

上述程式碼中,worker 就是一個 Web Workers 例項,執行的程式碼是 ../src/worker.js 路徑下的檔案。

收發訊息

Web Workers 用來執行非同步指令碼,只要掌握了它與主執行緒通訊的方式,就可以在指定時機執行非同步指令碼,並在執行完時將結果傳遞給主執行緒。

主執行緒接收發 Web Workers 訊息

const worker = new Worker("../src/worker.js");

worker.onmessage = e => {};

worker.postMessage("Marco!");
複製程式碼

每個 worker 例項通過 onmessage 接收訊息,通過 postMessage 傳送訊息。

Web Workers 收發主執行緒訊息

self.onmessage = e => {};

self.postMessage("Marco!");
複製程式碼

和主執行緒程式碼類似,在 Web Workers 程式碼中,也是 onmessage 接收訊息,這個訊息來自主執行緒或者其它 Workers。也可以通過 postMessage 傳送訊息。

銷燬 Web Workers

worker.terminate();
複製程式碼

文章內容就這麼多,是不是有寫太簡單了呢!筆者結合自己的使用經驗,再補充一些知識。

3 精讀

物件轉移(Transferable Objects)

物件轉移就是將物件引用零成本轉交給 Web Workers 的上下文,而不需要進行結構拷貝。

這裡要解釋的是,主執行緒與 Web Workers 之間的通訊,並不是物件引用的傳遞,而是序列化/反序列化的過程,當物件非常龐大時,序列化和反序列化都會消耗大量計算資源,降低執行速度。

精讀《談談 Web Workers》

上面的圖充分證明了,大物件傳遞,使用物件轉移各項指標都優於結構拷貝。

物件轉移使用方式很簡單,給 postMessage 增加一個引數,把物件引用傳過去即可:

var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
複製程式碼

瀏覽器相容性也不錯:Currently Chrome 17+, Firefox, Opera, Safari, IE10+。更具體內容,可以看 Transferable Objects: Lightning Fast!

需要注意的是,物件引用轉移後,原先上下文就無法訪問此物件了,需要在 Web Workers 再次將物件還原到主執行緒上下文後,主執行緒才能正常訪問被轉交的物件。

如何不用 JS 檔案建立 Web Workers

Web Workers 優勢這麼大,但用起來需要在同域下建立一個 JS 檔案實在不方便,尤其在前後端分離做的比較徹底的團隊,前端團隊能控制的僅僅是一個 JS 檔案。那麼下面給出幾個不用 JS 檔案,就建立 Web Workers 的方法:

webpack 外掛 - worker-loader

worker-loader 是一個 webpack 外掛,可以將一個普通 JS 檔案的全部依賴提取後打包並替換呼叫處,以 Blob 形式內聯在原始碼中。

import Worker from "worker-loader!./file.worker.js";

const worker = new Worker();
複製程式碼

上述程式碼的魔術在於,轉化成下面的方式執行:

const blob = new Blob([codeFromFileWorker], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
複製程式碼

Blob URL

第二種方式由第一種方式自然帶出:如果不想用 webpack 外掛,那自己通過 Blob 的方式建立也可以:

const code = `
  importScripts('https://xxx.com/xxx.js');
  self.onmessage = e => {};
`;

const blob = new Blob([code], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
複製程式碼

看上去程式碼更輕量一些,不過問題是當遇到複雜依賴時,如果不能把依賴都轉化為指令碼通過 importScripts 方式引用,就無法訪問到主執行緒環境中的包。如果真的遇到了這個問題,可以用第一種 webpack 外掛的方式解決,這個外掛會自動把檔案所有依賴都打包進原始碼。

管理 postMessage 佇列

為什麼 postMessage 會形成佇列,為什麼要管理它?

首先在 Web Workers 架構設計上就必須做成佇列,因為呼叫 postMessage 時,對應的 Web Workers 不一定完成了初始化,所以瀏覽器底層必須管理一個佇列,在 Web Workers 初始化完畢時,依次消費,這樣才能確保任何時候發出的 postMessage 都能被 Web Workers 接收到。

其次,為什麼要手動維護這個佇列,原因可能取決於如下幾點:

  • 業務原因,前面的 postMessage 還沒來得及消費,就不要傳送新的訊息,或者丟棄新的訊息,這時候需要通過雙向通訊拿到 Web Workers 的執行結果回執,手動控制佇列。
  • 效能原因,一般 Web Workers 都會被用來執行耗時的同步運算,如果運算時間比較長,那短期塞入多個訊息佇列是沒有意義的。

精讀《談談 Web Workers》

如上圖所示,對於每次使用者輸入都要進行的 SQL Parser 很耗時,及時放在 Web Workers 也可能導致將 Workers 撐爆到無響應,這是不僅要使用多 Workers 緩衝池,還要對待執行佇列進行過濾,因為使用者永遠只關心最後一次輸入的 Parser 結果。

由於 Web Workers 運算被卡住時,除了銷燬 Worker 沒有別的辦法,而銷燬 Worker 的成本比較高,不能對每一個使用者輸入都銷燬並新建 Web Workers,所以利用 Workers 緩衝池,當緩衝池滿了,新的消費佇列又進來的時候,可以銷燬全部 Workers 緩衝池,換一批新緩衝池重新消費使用者輸入。

4 總結

Web Workers 是拆解非同步計算的好幫手,vscode 網頁版也通過 Web Workers 非同步完成程式碼提示和高亮,筆者有對比過,發現 Web Workers 效能提升非常明顯。

管理好你的 Web Workers 訊息佇列,謹防同步計算讓 Web Workers 失去響應!建立一個智慧的訊息佇列,根據業務需求設計一個最好的佇列消費模型吧!

5 更多討論

討論地址是:精讀《談談 Web Workers》 · Issue #108 · dt-fe/weekly

如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

相關文章