一. Web Workers 客戶端應用
使用 JavaScript 創建 Web Worker 的步驟如下:
1.創建一個新的 JavaScript 文件,其中包含要在工作線程中運行的代碼(耗時任務)。該文件不應包含對 DOM 的引用,因為在工作線程中無法訪問 DOM。
2.在主 JavaScript 文件中,使用?Worker
?構造函數創建一個新的worker
對象。此構造函數接收一個參數,即在步驟 1 中創建的 JavaScript 文件的 URL。
?
const?worker?=?new?Worker('worker.js');
3.向worker
對象添加事件偵聽器以處理主線程和工作線程之間發送的消息。onmessage
?用于處理從工作線程發送來的消息,postMessage
?用于向工作線程發送消息。
?
worker.onmessage?=?function(event)?{console.log('Worker:?'?+?event.data);
};worker.postMessage('Hello,?worker!');
4.在 Web Worker 的 JavaScript 文件中,使用self
對象的onmessage
屬性添加一個事件監聽器來處理從主線程發出的消息。可以使用event.data
屬性訪問發送的消息數據。
?
self.onmessage?=?function(event)?{console.log('Main:?'?+?event.data);self.postMessage('Hello,?Main!');
};
接下來就運行應用并測試 Worker。可以在控制臺看到以下信息,表示主線程和 Worker 線程之間發送和接收了消息。
?
Main:Hello?worker!
Worker:Hello?Main!
我們可以使用terminate()
函數來終止一個工作線程,或者通過調用self
上的close()
函數使其自行終止。
?
//?從應用中終止一個工作線程
worker.terminate();
//?讓一個工作線程自行終止
self.close();
可以使用importScripts()
函數將庫或文件導入到工作線程中,該函數可以接受多個文件。以下示例將script1.js
和script2.js
加載到工作線程?worker.js
?中:
?
importScripts('script1.js','script2');
可以使用?onerror
函數來處理工作線程拋出的錯誤:
?
worker.onerror?=?function(err)?{console.log("遇到錯誤")
}
二. Web Workers 服務端應用
服務器端 JavaScript 運行時也支持 Web Worker:
-
Node.js 在版本 10 中實現了類似 Web Worker 的功能,稱為 Worker thread(工作進程)。
-
Deno 復制了 Web Worker API,因此語法與瀏覽器代碼完全相同。它還提供了兼容模式,可以填充 Node.js API,以便可以使用該運行時的工作線程語法。
-
Bun 將支持瀏覽器和 Node.js 的 Web Worker API。
1. 基本使用
要在 Node.js 中使用 Web Worker,主腳本必須定義一個?Worker
?對象,其中包含相對于項目根目錄的 Web Worker 腳本的名稱。第二個參數定義了一個對象,其中包含一個workerData
屬性,該屬性包含要發送的數據:
?
const?worker?=?new?Worker('./worker.js',?{workerData:?{?a:?1,?b:?2,?c:?3?}
});
與瀏覽器中的 Web Worker 不同, 它在啟動時無需運行worker.postMessage()
。如果需要的話,可以調用該方法并稍后發送更多數據,它會觸發parentPort.on('message')
事件處理程序:
?
parentPort.on('message',?e?=>?{console.log(e);
});
一旦工作線程完成處理,它會使用以下方法將結果數據發送回主線程:
?
parentPort.postMessage(result);
這將在在主腳本中觸發?message
?事件,主線程接收到 worker 返回的結果:
?
worker.on('message',?result?=>?{console.log(?result?);
});
在發送完消息后,worker 就會終止。這也會觸發一個exit
事件,如果希望運行清理或其他函數,可以利用這個事件:
?
worker.on('exit',?code?=>?{//...
});
除此之外,還支持其他事件處理:
-
messageerror
:當 worker 收到無法反序列化的數據時觸發。 -
online
:當 worker 開始執行時觸發。 -
error
:當 worker 腳本中發生 JavaScript 錯誤時觸發。
在服務端,一個單獨的 Node.js 腳本文件可以同時包含主線程和工作線程的代碼。腳本必須使用isMainThread
檢查自身是否在主線程上運行,然后將自身作為工作線程進行調用(可以在 ES 模塊中使用import.meta.url
作為文件引用,或者在 CommonJS 中使用__filename
)。
?
import?{?Worker,?isMainThread,?workerData,?parentPort?}?from?"node:worker_threads";if?(isMainThread)?{//?主線程const?worker?=?new?Worker(import.meta.url,?{workerData:?{?a:?1,?b:?2,?c:?3?}});worker.on('message',?msg?=>?{});worker.on('exit',?code?=>?{});
}
else?{//?工作線程const?result?=?runSomeProcess(?workerData?);parentPort.postMessage(result);
}
這種方式更快,并且對于小型、自包含的單腳本項目來說是一個選擇。如果是大型項目,將 worker 腳本文件分開會更容易維護。
2. 數據通信
主線程和工作線程之間的通信涉及到了數據序列化。可以使用表示固定長度原始二進制數據的SharedArrayBuffer
對象在線程之間共享數據。以下是一個示例,主線程定義了從 0 到 99 的 100 個數字元素,并將其發送給工作線程:
?
//?main.js
import?{?Worker?}?from?"node:worker_threads";constbuffer?=?new?SharedArrayBuffer(100?*?Int32Array.BYTES_PER_ELEMENT),value?=?new?Int32Array(buffer);value.forEach((v,i)?=>?value[i]?=?i);const?worker?=?new?Worker('./worker.js');worker.postMessage({?value?});
工作線程可以接收?value
對象:
?
//?worker.js
import?{?parentPort?}?from?'node:worker_threads';parentPort.on('message',?value?=>?{value[0]?=?100;
});
主線程或工作線程都可以更改值數組中的元素,數據將在兩個線程之間保持一致。這可能會提高性能,但有一些缺點:
-
只能共享整數數據。
-
可能仍需要通知另一個線程更改。
-
存在兩個線程可能同時更新同一值并且失去同步的風險。
3. Node.js 子進程
在 Node.js 中,除了使用工作線程外,還可以使用子進程來實現類似的功能。子進程用于啟動其他應用、傳遞數據并接收結果。它們與工作線程類似,但通常效率較低,進程開銷較大。
子進程和工作線程的選擇取決于具體的應用場景。如果只需要在 Node.js 中執行其他任務或命令,子進程是一種更好的選擇。但如果需要在 Node.js 中進行復雜的計算或處理任務,Web Worker 可能更適合。