在Web Worker与主线程之间进行通信时,应用postMessage是一种常见的形式。然而,在某些业务场景中,postMessage可能会显得不够简洁,因为它波及到手动序列化和反序列化数据,以及通过事件监听器解决音讯。以下是一些常见问题和解决方案,以简化在Web Worker与主线程之间的通信场景中应用postMessage的问题。

结构化克隆问题

在Web Worker与主线程之间传输数据时,应用postMessage()办法进行通信,浏览器会对传递的数据进行序列化和反序列化的过程,以便在不同的线程间传递数据。这个序列化和反序列化的过程就是结构化克隆(Structured Cloning)。

结构化克隆是一种浏览器内置的序列化和反序列化算法,它能够将简单的JavaScript对象、数组、字符串、数字、布尔值等数据类型转换成一个能够在不同线程间传递的二进制数据流,而后再将这个二进制数据流反序列化为与原始数据雷同的JavaScript对象。

结构化克隆有一些特点和限度:

  1. 反对的数据类型:结构化克隆反对包含对象、数组、字符串、数字、布尔值、日期、正则表达式、Blob、File、ImageData等常见的JavaScript数据类型。但并不反对函数、Map、Set、Symbol等一些非凡的JavaScript数据类型。
  2. 克隆整个对象:结构化克隆会克隆整个对象,包含对象的所有属性和办法。这可能会导致性能开销较大,尤其是在传输大规模数据时。
  3. 不共享内存:结构化克隆会生成一份残缺的正本,而不是共享内存。这意味着在主线程和Web Worker之间传递数据时,会产生复制的开销,并且对数据的批改在不同线程中是不共享的。
  4. 兼容性:结构化克隆在大多数古代浏览器中失去反对,但并不是所有浏览器都反对。一些老旧的浏览器可能不反对结构化克隆或者只反对局部数据类型的结构化克隆。

在传输过程中,当应用postMessage()办法传递数据时,浏览器会主动应用结构化克隆对数据进行序列化和反序列化的过程,以便在不同线程间传递数据,但结构化克隆可能会带来性能开销和兼容性问题,须要依据具体情况来抉择适合的解决方案。在不反对结构化克隆的浏览器下,应用postMessage()传输数据须要应用JSON对数据内容进行字符串转化和解析,这也会带来肯定的性能损耗和数据类型限度。

优化计划

  1. 宰割数据:将大规模的数据宰割成较小的块进行传递,而不是一次性传递整个数据。例如,能够将大型数组切割成多个小块,别离传递给Web Worker,而后在Web Worker中重新组合这些小块,从而缩小单次传递的数据量。
  2. 应用共享内存:共享内存是一种在Web Worker和主线程之间共享数据的形式,而无需进行复制。这样能够防止结构化克隆的性能开销。共享内存能够通过应用TypedArray和ArrayBuffer来实现,能够在主线程和Web Worker之间间接共享数据的援用,而不须要进行复制。须要留神的是,共享内存可能须要应用锁或其余同步机制来确保对共享数据的拜访是平安的。
  3. 应用其余序列化形式:除了结构化克隆,还能够思考应用其余的序列化形式,例如JSON.stringify和JSON.parse。尽管JSON序列化和反序列化可能比结构化克隆更慢,但它不会像结构化克隆一样复制整个数据(因仅反对局部数据类型,以及会忽视undefined的字段等),而是将数据转换为JSON字符串,并在接管方解析JSON字符串成JavaScript对象。这样能够肯定的防止复制大规模的数据,从而升高性能开销。
  4. 应用压缩算法:对于大规模的数据,能够思考应用压缩算法对数据进行压缩,从而减小数据的大小,升高传输的数据量。在接管方进行解压缩后再进行解决。常见的压缩算法有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');});

输入