Web?Woker與主線程通信場景下對postMessage的簡潔封裝詳解
Web Worker與主線程之間進行通信
使用postMessage
是一種常見的方式。然而,在某些業(yè)務(wù)場景中,postMessage
可能會顯得不夠簡潔,因為它涉及到手動序列化和反序列化數(shù)據(jù),以及通過事件監(jiān)聽器處理消息。以下是一些常見問題和解決方案,以簡化在Web Worker與主線程之間的通信場景中使用postMessage
的問題。
結(jié)構(gòu)化克隆問題
在Web Worker與主線程之間傳輸數(shù)據(jù)時,使用postMessage()方法進行通信,瀏覽器會對傳遞的數(shù)據(jù)進行序列化和反序列化的過程,以便在不同的線程間傳遞數(shù)據(jù)。這個序列化和反序列化的過程就是結(jié)構(gòu)化克?。⊿tructured Cloning)。
結(jié)構(gòu)化克隆是一種瀏覽器內(nèi)置的序列化和反序列化算法,它可以將復(fù)雜的JavaScript對象、數(shù)組、字符串、數(shù)字、布爾值等數(shù)據(jù)類型轉(zhuǎn)換成一個可以在不同線程間傳遞的二進制數(shù)據(jù)流,然后再將這個二進制數(shù)據(jù)流反序列化為與原始數(shù)據(jù)相同的JavaScript對象。
結(jié)構(gòu)化克隆有一些特點和限制:
- 支持的數(shù)據(jù)類型:結(jié)構(gòu)化克隆支持包括對象、數(shù)組、字符串、數(shù)字、布爾值、日期、正則表達式、Blob、File、ImageData等常見的JavaScript數(shù)據(jù)類型。但并不支持函數(shù)、Map、Set、Symbol等一些特殊的JavaScript數(shù)據(jù)類型。
- 克隆整個對象:結(jié)構(gòu)化克隆會克隆整個對象,包括對象的所有屬性和方法。這可能會導(dǎo)致性能開銷較大,尤其是在傳輸大規(guī)模數(shù)據(jù)時。
- 不共享內(nèi)存:結(jié)構(gòu)化克隆會生成一份完整的副本,而不是共享內(nèi)存。這意味著在主線程和Web Worker之間傳遞數(shù)據(jù)時,會產(chǎn)生復(fù)制的開銷,并且對數(shù)據(jù)的修改在不同線程中是不共享的。
- 兼容性:結(jié)構(gòu)化克隆在大多數(shù)現(xiàn)代瀏覽器中得到支持,但并不是所有瀏覽器都支持。一些老舊的瀏覽器可能不支持結(jié)構(gòu)化克隆或者只支持部分數(shù)據(jù)類型的結(jié)構(gòu)化克隆。
在傳輸過程中,當(dāng)使用postMessage()方法傳遞數(shù)據(jù)時,瀏覽器會自動使用結(jié)構(gòu)化克隆對數(shù)據(jù)進行序列化和反序列化的過程,以便在不同線程間傳遞數(shù)據(jù),但結(jié)構(gòu)化克隆可能會帶來性能開銷和兼容性問題,需要根據(jù)具體情況來選擇合適的解決方案。在不支持結(jié)構(gòu)化克隆的瀏覽器下,使用postMessage()傳輸數(shù)據(jù)需要使用JSON對數(shù)據(jù)內(nèi)容進行字符串轉(zhuǎn)化和解析,這也會帶來一定的性能損耗和數(shù)據(jù)類型限制。
優(yōu)化方案
- 分割數(shù)據(jù):將大規(guī)模的數(shù)據(jù)分割成較小的塊進行傳遞,而不是一次性傳遞整個數(shù)據(jù)。例如,可以將大型數(shù)組切割成多個小塊,分別傳遞給Web Worker,然后在Web Worker中重新組合這些小塊,從而減少單次傳遞的數(shù)據(jù)量。
- 使用共享內(nèi)存:共享內(nèi)存是一種在Web Worker和主線程之間共享數(shù)據(jù)的方式,而無需進行復(fù)制。這樣可以避免結(jié)構(gòu)化克隆的性能開銷。共享內(nèi)存可以通過使用TypedArray和ArrayBuffer來實現(xiàn),可以在主線程和Web Worker之間直接共享數(shù)據(jù)的引用,而不需要進行復(fù)制。需要注意的是,共享內(nèi)存可能需要使用鎖或其他同步機制來確保對共享數(shù)據(jù)的訪問是安全的。
- 使用其他序列化方式:除了結(jié)構(gòu)化克隆,還可以考慮使用其他的序列化方式,例如JSON.stringify和JSON.parse。雖然JSON序列化和反序列化可能比結(jié)構(gòu)化克隆更慢,但它不會像結(jié)構(gòu)化克隆一樣復(fù)制整個數(shù)據(jù)(因僅支持部分數(shù)據(jù)類型,以及會無視undefined的字段等),而是將數(shù)據(jù)轉(zhuǎn)換為JSON字符串,并在接收方解析JSON字符串成JavaScript對象。這樣可以一定的避免復(fù)制大規(guī)模的數(shù)據(jù),從而降低性能開銷。
- 使用壓縮算法:對于大規(guī)模的數(shù)據(jù),可以考慮使用壓縮算法對數(shù)據(jù)進行壓縮,從而減小數(shù)據(jù)的大小,降低傳輸?shù)臄?shù)據(jù)量。在接收方進行解壓縮后再進行處理。常見的壓縮算法有g(shù)zip、zlib等,可以在主線程和Web Worker之間使用這些算法對數(shù)據(jù)進行壓縮和解壓縮。
postMessage 簡單封裝
主進程封裝
// 定義一個 WorkerMessage 類,用于向 Worker 發(fā)送消息并處理返回結(jié)果 let canStructuredClone; export class WorkerMessage { constructor(workerUrl) { this.worker = new Worker(workerUrl); this.callbacks = new Map(); canStructuredClone === undefined && this.isStructuredCloneSupported(); // 監(jiān)聽從 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); }); } // 發(fā)送消息給 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}); }); } // 關(guān)閉 Worker terminate() { this.worker.terminate(); } // 判斷當(dāng)前瀏覽器是否支持結(jié)構(gòu)化克隆算法 isStructuredCloneSupported() { try { const obj = {data: 'Hello'}; const clonedObj = window.postMessage ? window.postMessage(obj, '*') : obj; return canStructuredClone = clonedObj !== obj; } catch (error) { // 捕獲到異常,說明瀏覽器不支持結(jié)構(gòu)化克隆 return canStructuredClone = false; } } }
在上面的代碼中,我們定義了一個名為 WorkerMessage
的類,用于向 Worker 發(fā)送消息并處理返回結(jié)果。在該類的構(gòu)造函數(shù)中,我們首先創(chuàng)建了一個 Worker 實例,并監(jiān)聽了 message 事件。我們使用一個 Map 對象來保存每個消息的回調(diào)函數(shù),以便后續(xù)能夠根據(jù)消息 ID 找到對應(yīng)的回調(diào)函數(shù)。當(dāng)從 Worker 返回的消息中包含了 ID 時,我們從 Map 中找到對應(yīng)的回調(diào)函數(shù),并根據(jù)消息的類型分別調(diào)用 resolve 和 reject 方法。在調(diào)用這些方法后,我們需要從 Map 中刪除對應(yīng)的回調(diào)函數(shù),以避免內(nèi)存泄漏。
在 WorkerMessage
類中,我們定義了一個 postMessage
方法,用于向 Worker 發(fā)送消息并處理返回結(jié)果。在該方法中,我們首先生成一個唯一的消息 ID,并構(gòu)造了要發(fā)送給 Worker 的消息。然后我們使用 worker.postMessage 方法發(fā)送該消息,并返回一個 Promise 對象,以便業(yè)務(wù)層進行異步處理。在該 Promise
對象中,我們使用 callbacks.set 方法將該消息 ID 和對應(yīng)的回調(diào)函數(shù)保存到 Map 中。
在 WorkerMessage
類中,我們還定義了一個 terminate
方法,用于關(guān)閉 Worker 實例。該方法會調(diào)用 worker.terminate
方法來關(guān)閉 Worker。
同時,我們使用 isStructuredCloneSupported
方法判斷當(dāng)前瀏覽器or環(huán)境是否支持結(jié)構(gòu)化克隆,以外部 canStructuredClone
進行標記,并只在對象首次實例化的時候進行復(fù)制。如果當(dāng)前瀏覽器不支持結(jié)構(gòu)化克隆,則postMessage
使用JSON.stringify
轉(zhuǎn)換成字符串。
子進程封裝類const res = this.configtype);
export class childMessage { constructor(self, config) { this.self = self; this.config = config; } // 監(jiān)聽從主線程傳來的消息 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?.(); } }); } }
這個子進程消息傳遞的類,通過監(jiān)聽主線程發(fā)送的消息,并使用傳入的 config
對象處理不同類型的消息,主進程通過指定執(zhí)行函數(shù)的type,由worker來調(diào)用制定的函數(shù)。其中,callback
參數(shù)是一個可選的回調(diào)函數(shù),在處理完一條消息后可以執(zhí)行。其中addEventListener(callback)
通過添加一個消息監(jiān)聽器,接收一個回調(diào)函數(shù)作為參數(shù)。在這個方法中,通過調(diào)用 addEventListener
方法,監(jiān)聽主線程發(fā)送過來的消息。然后對收到的消息進行處理,并將處理結(jié)果返回給主線程。如果結(jié)果是一個 Promise,則使用 then
方法處理異步結(jié)果,并將結(jié)果發(fā)送給主線程。如果結(jié)果是一個普通值,則直接將結(jié)果發(fā)送給主線程。在處理完一條消息后,會執(zhí)行可選的 callback
回調(diào)函數(shù)。
同時也使用了canStructuredClone
,如果瀏覽器支持結(jié)構(gòu)化克?。╯tructured clone)算法,則直接將 payload 傳給處理函數(shù)。否則,將 payload
進行 JSON 轉(zhuǎn)換,并將其傳給處理函數(shù)。
使用案例
主進程
// 創(chuàng)建一個 WorkerMessage 實例,并指定要加載的 Worker 文件路徑 import {WorkerMessage} from "../index.js"; console.log('WorkerMessage start') const worker = new WorkerMessage('./worker.js'); // 發(fā)送一個消息給 Worker,并處理返回結(jié)果 worker.postMessage({type: 'CALCULATE', payload: 10}).then( result => { console.log('計算結(jié)果:', result); }, error => { console.error('計算出錯:', error); } ); // 關(guān)閉 Worker 實例 // worker.terminate(); worker.postMessage({type: 'PLUS', payload: 10}).then( result => { console.log('計算結(jié)果:', result); }, error => { console.error('計算出錯:', error); } );
worker.js worker進程
import {childMessage} from "../index.js"; // 執(zhí)行計算的函數(shù) function doCalculate(num) { // 這里可以執(zhí)行一些復(fù)雜的計算任務(wù) 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 }); // 起到分發(fā)執(zhí)行的效果 child.addEventListener(()=>{ console.log('worker is listened'); });
輸出
以上就是Web Woker與主線程通信場景下對postMessage的簡潔封裝詳解的詳細內(nèi)容,更多關(guān)于Web Woker封裝postMessage的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS+HTML5 Canvas實現(xiàn)簡單的寫字板功能示例
這篇文章主要介紹了JS+HTML5 Canvas實現(xiàn)簡單的寫字板功能,結(jié)合實例形式分析了js結(jié)合HTML5 canvas特性的圖形繪制相關(guān)操作技巧,需要的朋友可以參考下2018-08-08Javascript 詳解封裝from表單數(shù)據(jù)為json串進行ajax提交
這篇文章主要介紹了Javascript 詳解封裝from表單數(shù)據(jù)為json串進行ajax提交的相關(guān)資料,需要的朋友可以參考下2017-03-03淺談javascript六種數(shù)據(jù)類型以及特殊注意點
這篇文章主要介紹了javascript六種數(shù)據(jù)類型以及特殊注意點,有需要的朋友可以參考一下2013-12-12百度地圖JavascriptApi Marker平滑移動及車頭指向行徑方向
本文主要介紹了百度地圖JavascriptApi Marker平滑移動及車頭指向行徑方向的相關(guān)知識。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03