現在,您可以透過 Web Worker 中的 JavaScript 模組,更輕鬆地將繁重的工作移至背景執行緒。
JavaScript 是單一執行緒,也就是一次只能執行一項作業。這種做法很直觀,適用於網頁上的許多情況,但如果需要執行資料處理、剖析、運算或分析等繁重工作,就可能發生問題。隨著網路上提供的複雜應用程式越來越多,多執行緒處理的需求也隨之增加。
在網頁平台上,執行緒和並行處理的主要原始物件是 Web Workers API。Worker 是作業系統執行緒之上的輕量抽象層,可提供訊息傳遞 API,用於執行緒間通訊。在執行高成本運算或處理大型資料集時,這項功能非常實用,可讓主執行緒順暢執行,同時在一或多個背景執行緒上執行高成本作業。
以下是 worker 的典型用法範例,其中 worker 指令碼會監聽主執行緒的訊息,並傳回自己的訊息做為回應:
page.js:
const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
console.log(e.data);
});
worker.postMessage('hello');
worker.js:
addEventListener('message', e => {
if (e.data === 'hello') {
postMessage('world');
}
});
Web Worker API 已在大多數瀏覽器中推出超過十年。雖然這代表 worker 具備出色的瀏覽器支援,且經過充分最佳化,但也代表 worker 的歷史比 JavaScript 模組更悠久。由於設計 worker 時沒有模組系統,因此將程式碼載入 worker 和編寫指令碼的 API,仍與 2009 年常見的同步指令碼載入方法類似。
歷史記錄:傳統工作站
Worker 建構函式會採用傳統指令碼網址,該網址與文件網址相關。它會立即傳回新 worker 執行個體的參照,該執行個體會公開訊息介面以及 terminate() 方法,該方法會立即停止並刪除 worker。
const worker = new Worker('worker.js');
Web worker 內提供 importScripts() 函式,可用於載入額外程式碼,但會暫停 worker 執行,以便擷取及評估各個指令碼。此外,它也會在全域範圍內執行指令碼,就像傳統的 <script> 標記一樣,這表示一個指令碼中的變數可能會被另一個指令碼中的變數覆寫。
worker.js:
importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
// global to the whole worker
function sayHello() {
return 'world';
}
因此,Web Worker 過去對應用程式架構造成了極大影響。開發人員必須建立巧妙的工具和解決方法,才能在不放棄現代開發做法的情況下使用 Web Worker。舉例來說,webpack 等套件組合器會將小型模組載入器實作嵌入產生的程式碼中,該程式碼會使用 importScripts() 載入程式碼,但會將模組包裝在函式中,以避免變數衝突,並模擬依附元件匯入和匯出。
輸入模組 worker
Chrome 80 推出新的 Web Worker 模式,具備 JavaScript 模組的人體工學和效能優勢,稱為模組 Worker。Worker 建構函式現在接受新的 {type:"module"} 選項,可變更指令碼載入和執行方式,以符合 <script type="module">。
const worker = new Worker('worker.js', {
type: 'module'
});
由於模組 worker 是標準 JavaScript 模組,因此可以使用 import 和 export 陳述式。與所有 JavaScript 模組一樣,依附元件在特定情境 (主執行緒、worker 等) 中只會執行一次,日後所有匯入作業都會參照已執行的模組執行個體。瀏覽器也會最佳化 JavaScript 模組的載入和執行。模組的依附元件可以在模組執行前載入,因此整個模組樹狀結構可以平行載入。模組載入也會快取剖析過的程式碼,這表示在主執行緒和 worker 中使用的模組只需要剖析一次。
改用 JavaScript 模組後,您也可以使用動態匯入功能延遲載入程式碼,而不會阻礙 worker 執行。動態匯入比使用 importScripts() 載入依附元件更明確,因為系統會傳回匯入模組的匯出內容,而不是依賴全域變數。
worker.js:
import { sayHello } from './greet.js';
addEventListener('message', e => {
postMessage(sayHello());
});
greet.js:
import greetings from './data.js';
export function sayHello() {
return greetings.hello;
}
為確保效能良好,模組 worker 無法使用舊的 importScripts() 方法。改用 JavaScript 模組後,所有程式碼都會在嚴格模式下載入。另一項值得注意的變更是,JavaScript 模組頂層範圍中的 this 值為 undefined,而在傳統 worker 中,該值為 worker 的全域範圍。幸好,我們一直有 self 全域,可提供全域範圍的參照。適用於所有類型的 worker (包括 Service Worker) 以及 DOM。
使用 modulepreload 預先載入 worker
模組 worker 帶來的一項重大效能改善,就是能夠預先載入 worker 及其依附元件。使用模組 worker 時,指令碼會以標準 JavaScript 模組的形式載入及執行,也就是說,可以使用 modulepreload 預先載入,甚至預先剖析:
<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">
<script>
addEventListener('load', () => {
// our worker code is likely already parsed and ready to execute!
const worker = new Worker('worker.js', { type: 'module' });
});
</script>
主執行緒和模組 worker 也可以使用預先載入的模組。這適用於在兩種情境中匯入的模組,或無法預先得知模組是否會在主執行緒或 worker 中使用的情況。
以往,預先載入 Web Worker 指令碼的選項有限,且不一定可靠。傳統 worker 有自己的「worker」資源類型可供預先載入,但沒有瀏覽器實作 <link rel="preload" as="worker">。因此,預先載入 Web Worker 的主要技術是使用 <link rel="prefetch">,這完全仰賴 HTTP 快取。如果搭配正確的快取標頭使用,就能避免 worker 例項化必須等待下載 worker 指令碼。不過,與 modulepreload 不同的是,這項技術不支援預先載入依附元件或預先剖析。
SharedWorker 呢?
自 Chrome 83 起,共用 worker 已更新,支援 JavaScript 模組。與專屬 worker 相同,使用 {type:"module"} 選項建構共用 worker 時,現在會將 worker 指令碼載入為模組,而非傳統指令碼:
const worker = new SharedWorker('/worker.js', {
type: 'module'
});
在支援 JavaScript 模組之前,SharedWorker() 建構函式只預期網址和選用的 name 引數。這項功能仍適用於傳統共用 worker,但建立模組共用 worker 時,必須使用新的 options 引數。可用選項與專屬 worker 相同,包括取代先前 name 引數的 name 選項。
Service Worker 呢?
Service Worker 規格已更新,支援接受 JavaScript 模組做為進入點,並使用與模組 worker 相同的 {type:"module"} 選項,但這項變更尚未在瀏覽器中實作。屆時,您可以使用下列程式碼,透過 JavaScript 模組例項化 Service Worker:
navigator.serviceWorker.register('/sw.js', {
type: 'module'
});
規格更新後,瀏覽器也開始實作新行為。這需要一些時間,因為將 JavaScript 模組帶入 Service Worker 時,會有一些額外的複雜問題。Service Worker 註冊程序需要比較匯入的指令碼與先前的快取版本,判斷是否要觸發更新,而這項作業必須在 Service Worker 使用 JavaScript 模組時實作。此外,在某些情況下,Service Worker 檢查更新時,必須能夠略過指令碼的快取。
其他資源和延伸閱讀
- 功能狀態、瀏覽器共識和標準化
- 原始模組 worker 規格新增內容
- 共用 worker 的 JavaScript 模組
- Service Worker 的 JavaScript 模組:Chrome 導入狀態