在Web Worker與主線程之間進行通信時,使用postMessage是一種常見的方式。然而,在某些業務場景中,postMessage可能會顯得不夠簡潔,因為它涉及到手動序列化和反序列化數據,以及通過事件監聽器處理消息。以下是一些常見問題和解決方案,以簡化在Web Worker與主線程之間的通信場景中使用postMessage的問題。
結構化克隆問題
在Web Worker與主線程之間傳輸數據時,使用postMessage()方法進行通信,瀏覽器會對傳遞的數據進行序列化和反序列化的過程,以便在不同的線程間傳遞數據。這個序列化和反序列化的過程就是結構化克隆(Structured Cloning)。
結構化克隆是一種瀏覽器內置的序列化和反序列化算法,它可以將複雜的JavaScript對象、數組、字符串、數字、布爾值等數據類型轉換成一個可以在不同線程間傳遞的二進制數據流,然後再將這個二進制數據流反序列化為與原始數據相同的JavaScript對象。
結構化克隆有一些特點和限制:
- 支持的數據類型:結構化克隆支持包括對象、數組、字符串、數字、布爾值、日期、正則表達式、Blob、File、ImageData等常見的JavaScript數據類型。但並不支持函數、Map、Set、Symbol等一些特殊的JavaScript數據類型。
- 克隆整個對象:結構化克隆會克隆整個對象,包括對象的所有屬性和方法。這可能會導致性能開銷較大,尤其是在傳輸大規模數據時。
- 不共享內存:結構化克隆會生成一份完整的副本,而不是共享內存。這意味着在主線程和Web Worker之間傳遞數據時,會產生複製的開銷,並且對數據的修改在不同線程中是不共享的。
- 兼容性:結構化克隆在大多數現代瀏覽器中得到支持,但並不是所有瀏覽器都支持。一些老舊的瀏覽器可能不支持結構化克隆或者只支持部分數據類型的結構化克隆。
在傳輸過程中,當使用postMessage()方法傳遞數據時,瀏覽器會自動使用結構化克隆對數據進行序列化和反序列化的過程,以便在不同線程間傳遞數據,但結構化克隆可能會帶來性能開銷和兼容性問題,需要根據具體情況來選擇合適的解決方案。在不支持結構化克隆的瀏覽器下,使用postMessage()傳輸數據需要使用JSON對數據內容進行字符串轉化和解析,這也會帶來一定的性能損耗和數據類型限制。
優化方案
- 分割數據:將大規模的數據分割成較小的塊進行傳遞,而不是一次性傳遞整個數據。例如,可以將大型數組切割成多個小塊,分別傳遞給Web Worker,然後在Web Worker中重新組合這些小塊,從而減少單次傳遞的數據量。
- 使用共享內存:共享內存是一種在Web Worker和主線程之間共享數據的方式,而無需進行復制。這樣可以避免結構化克隆的性能開銷。共享內存可以通過使用TypedArray和ArrayBuffer來實現,可以在主線程和Web Worker之間直接共享數據的引用,而不需要進行復制。需要注意的是,共享內存可能需要使用鎖或其他同步機制來確保對共享數據的訪問是安全的。
- 使用其他序列化方式:除了結構化克隆,還可以考慮使用其他的序列化方式,例如JSON.stringify和JSON.parse。雖然JSON序列化和反序列化可能比結構化克隆更慢,但它不會像結構化克隆一樣複製整個數據(因僅支持部分數據類型,以及會無視undefined的字段等),而是將數據轉換為JSON字符串,並在接收方解析JSON字符串成JavaScript對象。這樣可以一定的避免複製大規模的數據,從而降低性能開銷。
- 使用壓縮算法:對於大規模的數據,可以考慮使用壓縮算法對數據進行壓縮,從而減小數據的大小,降低傳輸的數據量。在接收方進行解壓縮後再進行處理。常見的壓縮算法有gzip、zlib等,可以在主線程和Web Worker之間使用這些算法對數據進行壓縮和解壓縮。
postMessage 簡單封裝
主進程封裝
// 定義一個 WorkerMessage 類,用於向 Worker 發送消息並處理返回結果
let canStructuredClone;
export class WorkerMessage {
constructor(workerUrl) {
this.worker = new Worker(workerUrl);
this.callbacks = new Map();
canStructuredClone === undefined && this.isStructuredCloneSupported();
// 監聽從 Worker 返回的消息
this.worker.addEventListener('message', event => {
const {id, type, payload} = event.data;
const callback = this.callbacks.get(id);
if (!callback) {
console.warn(`未知的消息 ID:${id}`);
return;
}
switch (type) {
case 'SUCCESS':
callback.resolve(payload);
break;
case 'ERROR':
callback.reject(payload);
break;
default:
console.warn('未知的消息類型:', type);
}
this.callbacks.delete(id);
});
}
// 發送消息給 Worker
postMessage(payload) {
const id = Date.now().toString(36) + Math.random().toString(36).substr(2);
const message = canStructuredClone ? {id, payload} : JSON.stringify({id, payload})
this.worker.postMessage(message);
return new Promise((resolve, reject) => {
this.callbacks.set(id, {resolve, reject});
});
}
// 關閉 Worker
terminate() {
this.worker.terminate();
}
// 判斷當前瀏覽器是否支持結構化克隆算法
isStructuredCloneSupported() {
try {
const obj = {data: 'Hello'};
const clonedObj = window.postMessage ? window.postMessage(obj, '*') : obj;
return canStructuredClone = clonedObj !== obj;
} catch (error) {
// 捕獲到異常,説明瀏覽器不支持結構化克隆
return canStructuredClone = false;
}
}
}
在上面的代碼中,我們定義了一個名為 WorkerMessage 的類,用於向 Worker 發送消息並處理返回結果。在該類的構造函數中,我們首先創建了一個 Worker 實例,並監聽了 message 事件。我們使用一個 Map 對象來保存每個消息的回調函數,以便後續能夠根據消息 ID 找到對應的回調函數。當從 Worker 返回的消息中包含了 ID 時,我們從 Map 中找到對應的回調函數,並根據消息的類型分別調用 resolve 和 reject 方法。在調用這些方法後,我們需要從 Map 中刪除對應的回調函數,以避免內存泄漏。
在 WorkerMessage 類中,我們定義了一個 postMessage 方法,用於向 Worker 發送消息並處理返回結果。在該方法中,我們首先生成一個唯一的消息 ID,並構造了要發送給 Worker 的消息。然後我們使用 worker.postMessage 方法發送該消息,並返回一個 Promise 對象,以便業務層進行異步處理。在該 Promise 對象中,我們使用 callbacks.set 方法將該消息 ID 和對應的回調函數保存到 Map 中。
在 WorkerMessage 類中,我們還定義了一個 terminate 方法,用於關閉 Worker 實例。該方法會調用 worker.terminate 方法來關閉 Worker。
同時,我們使用 isStructuredCloneSupported 方法判斷當前瀏覽器or環境是否支持結構化克隆,以外部 canStructuredClone 進行標記,並只在對象首次實例化的時候進行復制。如果當前瀏覽器不支持結構化克隆,則postMessage使用JSON.stringify轉換成字符串。
子進程封裝類const res = this.configtype);
export class childMessage {
constructor(self, config) {
this.self = self;
this.config = config;
}
// 監聽從主線程傳來的消息
addEventListener(callback) {
this.self.addEventListener('message', event => {
const {id, payload} = event.data;
const {type} = payload;
try {
const res = this.config[type](canStructuredClone ? payload.payload : JSON.parse(payload.payload));
if (res instanceof Promise) {
res.then(data => {
this.self.postMessage({
id,
type: 'SUCCESS',
payload: canStructuredClone ? data : JSON.stringify(data)
});
}).catch(e => {
this.self.postMessage({id, type: 'ERROR', payload: e.toString()});
});
} else {
this.self.postMessage({
id,
type: 'SUCCESS',
payload: canStructuredClone ? res : JSON.stringify(res)
});
}
} catch (e) {
this.self.postMessage({id, type: 'ERROR', payload: e.toString()});
} finally {
callback?.();
}
});
}
}
這個子進程消息傳遞的類,通過監聽主線程發送的消息,並使用傳入的 config 對象處理不同類型的消息,主進程通過指定執行函數的type,由worker來調用制定的函數。其中,callback 參數是一個可選的回調函數,在處理完一條消息後可以執行。其中addEventListener(callback)通過添加一個消息監聽器,接收一個回調函數作為參數。在這個方法中,通過調用 addEventListener 方法,監聽主線程發送過來的消息。然後對收到的消息進行處理,並將處理結果返回給主線程。如果結果是一個 Promise,則使用 then 方法處理異步結果,並將結果發送給主線程。如果結果是一個普通值,則直接將結果發送給主線程。在處理完一條消息後,會執行可選的 callback 回調函數。
同時也使用了canStructuredClone,如果瀏覽器支持結構化克隆(structured clone)算法,則直接將 payload 傳給處理函數。否則,將 payload 進行 JSON 轉換,並將其傳給處理函數。
使用案例
主進程
// 創建一個 WorkerMessage 實例,並指定要加載的 Worker 文件路徑
import {WorkerMessage} from "../index.js";
console.log('WorkerMessage start')
const worker = new WorkerMessage('./worker.js');
// 發送一個消息給 Worker,並處理返回結果
worker.postMessage({type: 'CALCULATE', payload: 10}).then(
result => {
console.log('計算結果:', result);
},
error => {
console.error('計算出錯:', error);
}
);
// 關閉 Worker 實例
// worker.terminate();
worker.postMessage({type: 'PLUS', payload: 10}).then(
result => {
console.log('計算結果:', result);
},
error => {
console.error('計算出錯:', error);
}
);
worker.js worker進程
import {childMessage} from "../index.js";
// 執行計算的函數
function doCalculate(num) {
// 這裏可以執行一些複雜的計算任務
return num * 2;
}
function doPlus(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num + 1);
}, 1000);
});
}
const child = new childMessage(self, {
CALCULATE: doCalculate,
PLUS: doPlus
});
// 起到分發執行的效果
child.addEventListener(()=>{
console.log('worker is listened');
});