JavaScript 多线程利器 webWorker
JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
webWorker 的特点
一、同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
二、DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用 document、window、parent 这些对象。但是,Worker 线程可以 navigator 对象和 location 对象。
三、通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
四、脚本限制
Worker 线程不能执行 alert()方法和 confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
五、文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
基本用法
主线程
window 对象上存在 Worker 构造函数,通过调用创建一个 worker 进程:
/**
* worker 进程代码需要单独放入一个 js 文件
* 参数一:worker 进程代码路径
* 参数二:worker 进程名称,可能存在多个 worker 进程,取名进行区别
*/
const worker = new Worker("./worker.js",{ name: "myWorker" });
主进程主动与 worker 进程通信:
// 通过实例化对象调用 postMessage 方法,给 worker 发送消息
worker.postMessage("Hello");
主进程接收 worker 进程的消息:
// 通过实例化对象注册 message 事件接收 worker 进程的消息
worker.addEventListener("message", (event) => {
console.log(event.data);
// Do something...
});
同时主进程也可捕获先关的错误信息:
// 捕获错误事件
worker.addEventListener("error", (error) => {
console.log(error);
});
// 捕获使用 postMessage 方法造成的错误事件
worker.addEventListener("messageerror", (error) => {
console.log(error);
});
如果需要关闭主进程的 worker 连接,一般在使用完成后为了节省系统资源:
worker.terminate();
worker 进程
在 worker 进程中,全局对象为 self。
worker 进程接收主进程的消息:
self.addEventListener("message", (event) => {
console.log(event.data);
// Do something...
});
worker 进程主动与主进程通信:
self.postMessage("Hi");
worker 进程也可以进行捕获错误:
// 捕获错误事件
self.addEventListener("error", (error) => {
console.log(error);
});
// 捕获使用 postMessage 方法造成的错误事件
self.addEventListener("messageerror", (error) => {
console.log(error);
});
在 worker 进程完成相关任务后,也可以主动进行关闭:
self.close();
另外,与主进程语法不一样的地方是 worker 进程在引入其它 js 文件时,需要使用 importScripts 方法:
importScripts("script1.js");
vue 中使用 webWorker
需要预先安装 worker-loader:
npm install worker-loader --save-dev
在 vue.config.js 文件中进行加载器的配置:
module.exports = {
chainWebpack: (config) => {
config.module
.rule("worker")
.test(/\.worker\.js$/)
.use("worker")
.loader("worker-loader")
.options({
// 将 worker 内联为 blob
inline: true,
// 需要支持非 worker 环境的回退
fallback: false,
// 输出自定义文件名称
name: "WorkerName.[hash].js",
// worker 文件的路径
publicPath: "/workers/"
});
// 解决 worker 文件使用 window 问题
config.output.globalObject("this");
}
};
在 vue 中引入使用:
import Worker from "./file.worker.js";
const worker = new Worker();
worker.postMessage({ a: 1 });
worker.onmessage = function (event) {};
worker.addEventListener("message", function (event) {});
数据通信
主线程与 Worker 之间的通信内容,可以是文本,也可以是对象。需要注意的是,这种通信是拷贝关系,即是传值而不是传址,Worker 对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。
主线程与 Worker 之间也可以交换二进制数据,比如 File、Blob、ArrayBuffer 等类型,也可以在线程之间发送。
// 主线程
const uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (let i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 线程
self.onmessage = function (e) {
const uInt8Array = e.data;
postMessage(
"Inside worker.js: uInt8Array.toString() = " + uInt8Array.toString()
);
postMessage(
"Inside worker.js: uInt8Array.byteLength = " + uInt8Array.byteLength
);
};
但是,拷贝方式发送二进制数据,会造成性能问题。比如,主线程向 Worker 发送一个 500MB 文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript 允许主线程把二进制数据直接转移给子线程,但是一旦转移,主线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面。这种转移数据的方法,叫做 Transferable Objects。这使得主线程可以快速把数据交给 Worker,对于影像处理、声音处理、3D 运算等就非常方便了,不会产生性能负担。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
const ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);