天天看點

淺談Web Worker和Service worker

Javascript 由于它的單線程特性,任何“重量”操作都會阻塞主線程,讓我們的應用變得卡頓、看起來沒有響應。為了提升性能和體驗,現代浏覽器允許我們将一些工作交給 Worker,把原本的單線程應用變成多線程運作。

浏覽器中提供了 3 種 Worker,分别是:

  1. Web worker—— 包含專用 worker及共享 worker
  2. Service worker
  3. Worklet—— 包含PaintWorklet、AudioWorklet、AnimationWorklet、LayoutWorklet。

Worklet 似乎是專為媒體之類的應用設計,且仍處于試驗狀态。是以,本篇内容将隻探讨更通用的 Web worker 及 Service worker。

Web worker

Web worker 特别适用于背景跑腳本。現在的網頁都可以注冊多個 Worker,讓不同的任務在各自獨立的環境中完成。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-SmWroyUz-1666585948825)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4d9439fa4d074d5f85b8389ea839360a~tplv-k3u1fbpfcp-zoom-1.image)]

(圖檔來自Workers Overview)

用Worker API 建立一個頁面專屬線程的方法如下:

如果同源的不同标簽想共享一個線程,則可以用SharedWorker API:

這兩種 Worker 的用法近似,但在頁面和 Worker 的連接配接上,共享 Worker 是通過端口來通信的。是以在注冊共享 Worker 的時候必須指定一個可用的端口。然後在主線程中可以如下發送和接收消息:

mySharedWorker.port.postMessage("哈喽Worker!");
mySharedWorker.port.onmessage = (e) => {
  console.log("收到消息:", e.data);
};
           

在共享 Worker 跑的腳本中可以如下處理消息:

onconnect = (e) => {
  const port = e.ports[0];
  port.onmessage = (e) => {
    port.postMessage('收到消息 "${e.data}"`);
  };
}
           

專屬 Worker 的通信則比共享 Worker 的更直覺簡潔。在主線程中可以如下發送和接收消息:

myWorker.postMessage('哈喽Worker!');
myWorker.onMessage = (e) => {
  console.log('收到消息:', e.data);
};
           

在專屬 Worker 中可以如下處理消息:

onmessage = (e) => {
  postMessage('收到消息:"${e.data}"`);
};
           

此外,你可能還注意到一個差別是專屬 Worker 與頁面綁定在一起,是以頁面關閉的話專屬 Worker 也會被終止。

Service worker

Service worker 相當于是浏覽器在網頁和伺服器通信中插入的一個“中間層”。是以它可以操作伺服器請求,但由于安全和隐私性的考慮,浏覽器對可以操作的範圍有非常大的限制。

淺談Web Worker和Service worker

(圖檔來自Workers Overview)

不同于 Web worker,它有一個更複雜的生命周期:

  1. 在主線程中用

    navigator.serviceWorker.register()

    注冊 Service worker,然後浏覽器異步下載下傳 worker 用的腳本;
  2. 如果這個 Service worker 是新注冊的,安裝下載下傳腳本。否則就更新已存在的腳本;
  3. 如果在這個 Service worker 聲明管轄的範圍内沒有其他舊 worker 正在控制用戶端,激活 worker;
  4. 如果腳本已有新版本替代,棄置舊腳本。

Service worker 擁有一些列API 及事件來做如下的事情:

  • 緩存資料
  • 攔截請求
  • 管理浏覽器通知

基于這些能力,我們可以做很多有意思的事情。容我待會再聊。

他們的限制

Web worker 和 Service worker 功能強大,但也有不少限制。其中包括:

無權通路 DOM、window 對象和其它一些可能涉及隐私的 API,如 localStorage。這帶來的有點事 Worker 的運作環境與頁面的運作環境各自獨立,互不打擾。然而這也意味着如果你想讓 Worker 幫忙幹些 DOM 相關的費力活是很麻煩的。

主線程和 Worker 之間通信的資訊是個副本。這個副本用了一個算法叫the structured clone algorithm。這算法有點類似于

JSON.stringify()

JSON.parse()

,有很多資料類型在克隆的過程中被抹掉。且不同于 JSON 的接口,這個算法會直接讓不支援的資料 key 都消失,甚至有時候抛出異常。

僅提供對 ES 子產品的有限支援。雖說現代浏覽器已大部分支援直接運作 ES 子產品,但在 Worker 裡它還是試驗中的狀态,支援的浏覽器也很少。

As of Mar 1, 2019, only Chrome 80+ supports this feature, while Firefox has an open feature request. No other browsers are known to have support for production usage of worker scripts written as modules.

在支援的浏覽器中,你可以如下打開一個有 ES 子產品支援的 Worker:

new Worker("worker.js", {
  type: "module",
});
           

但含有關鍵字

import

export

的腳本并不能很好地工作,甚至會報錯終止 Worker 的運作。

還有一個槽點是在 Worker 裡打開一個新 Worker 這個在文檔中有所提及的功能幾乎沒法用。有些第三方庫比如

esbuild-wasm

内置了打開 Worker 運作的邏輯,那由這些庫提供的功能都無法在 Worker 中調用的話,組織代碼起來比較費力,需要讓主線程幫忙轉發消息。

繼續閱讀