乐趣区

关于前端:Web-Woker-与主线程通信场景下对postMessage的简洁封装

在 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');
});

输入

退出移动版