1 引言
本週精讀的文章是 speedy-introduction-to-web-workers,是一篇 Web Workers 快速入門的文章,借精讀這篇文章的機會,談談對 Web Workers 的理解與運用。
2 概述
就像分工,你只負責編碼,而你的朋友負責設計,那你就可以專心把自己的事情做好,而且更快速的完成任務。
本文通過一個比方,描述了 Web Workers 的兩大特徵:
- 高效。
- 並行。
因為瀏覽器是單執行緒的,任何大量耗時的 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 之間的通訊,並不是物件引用的傳遞,而是序列化/反序列化的過程,當物件非常龐大時,序列化和反序列化都會消耗大量計算資源,降低執行速度。
上面的圖充分證明了,大物件傳遞,使用物件轉移各項指標都優於結構拷貝。
物件轉移使用方式很簡單,給 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 都會被用來執行耗時的同步運算,如果運算時間比較長,那短期塞入多個訊息佇列是沒有意義的。
如上圖所示,對於每次使用者輸入都要進行的 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 更多討論
如果你想參與討論,請點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。