众所周知,JavaScript 是单线程的,但是随着 Web 技术的发展,我们越来越需要在 Web 应用中处理大量的计算任务,比如图像处理、数据分析等。为了解决这个问题,Web Workers 应运而生。Web Worker 是浏览器提供的一种 JavaScript 多线程机制,允许在主线程之外运行代码,从而避免阻塞 UI 渲染。
Web Worker 基础使用
我们可以使用 new Worker(url) 来创建一个 Web Worker,其中 url 是一个 JavaScript 文件的路径,该文件包含了需要在 Web Worker 中运行的代码。例如:
const worker = new Worker("worker.js");
// 错误处理
worker.onerror = function (event) {
console.error("Error in Worker: ", event.message);
};
// 终止 Worker
worker1.terminate();
注意: Worker 线程无法读取本地文件,所以不能打开本机的文件系统(file://)所加载的脚本,必须来自网络。
Worker 线程与主线程通信
Web Worker 与主线程之间可以通过 postMessage 和 onmessage 事件进行通信。主线程可以通过 postMessage 方法向 Worker 线程发送消息,Worker 线程可以通过 onmessage 事件监听主线程发送的消息。例如:
mian.js
// 主线程
const worker = new Worker("./worker.js");
worker.onmessage = function (event) {
console.log("Received message from worker:", event.data);
};
worker.postMessage("Hello, worker!");
worker.js
// Worker 线程
self.onmessage = function (event) {
console.log("Received message from main thread:", event.data);
self.postMessage("Hello, main thread!");
};
Worker 中的 API
self.close(); // 关闭 Worker
self.postMessage(message, [transfer]); // 向主线程发送消息
self.onmessage = function (event) {}; // 监听主线程发送的消息
self.onerror = function (event) {}; // 监听 Worker 线程中的错误
self.importScripts(url); // 导入其他脚本
Worker 线程与主线程共享数据
一般情况下,Web Worker 与主线程之间无法直接共享数据,但可以通过几种方式解决
默认数据传递(结构化克隆)
就是上面刚说到的通过 postMessage 传递数据,通过这种方式传递的数据会被深拷贝到目标线程,也就是说这种通信是拷贝关系,是传值而不是传址,线程对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给 Worker,后者再将它还原。
注意 :
postMessage方法只能传递可序列化的数据,对于不可序列化的数据(如函数、DOM 元素等)无法传递。
Transferable objects(转移)
ArrayBuffer、File、Blob这些也是可以直接传递给线程的,但是这些二进制数据在拷贝的过程中,性能开销较大,所以浏览器提供了 Transferable objects 语义,允许主线程把数据所有权转交给 Worker 线程,两者之间不再发生数据拷贝,也就不会发生阻塞主线程的情况。
mian.js
// 主线程
const worker = new Worker("./worker.js");
const buffer = new ArrayBuffer(1024);
console.log("初始化buffer: ", buffer.byteLength); // 1024
worker.postMessage(buffer, [buffer]); // // 转移后,主线程无法再访问 buffer
console.log("传递后buffer: ", buffer.byteLength); // 0
worker.js
// Worker 线程
self.onmessage = function (event) {
const buffer = event.data; // 可直接获取到原始的buffer
console.log(buffer.byteLength); // 1024
};
SharedArrayBuffer
SharedArrayBuffer 是一种特殊的 ArrayBuffer,允许多个线程共享同一块内存。主线程和 Worker 线程都可以通过 SharedArrayBuffer 对象来访问和修改共享内存,从而实现数据的共享和同步。但是需要注意必须满足以下两点:
- 安全的上下文 必须在 HTTPS 或 localhost 下使用
- 垮源隔离 需要在响应头中设置:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
mian.js
// 主线程
const worker = new Worker("./worker.js");
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Uint8Array(sharedBuffer);
for (let i = 0; i < sharedArray.length; i++) {
sharedArray[i] = i;
}
worker.postMessage(sharedBuffer);
worker.js
// Worker 线程
self.onmessage = function (event) {
const sharedBuffer = event.data;
const sharedArray = new Uint8Array(sharedBuffer);
console.log(sharedArray);
};
同页面 Web Worker
我们刚开始说过,new Worker(url) 需要一个 js 文件路径,但是我们也可以直接在当前页面创建一个 Worker,只需要将 new Worker() 改为 new Worker(URL.createObjectURL(new Blob([workerSource], { type: "text/javascript" })),其中 workerSource 是 Worker 的代码,例如:
const workerSource = `
self.onmessage = function (event) {
console.log("Received message from main thread:", event.data);
self.postMessage("Hello, main thread!");
}`;
const worker = new Worker(
URL.createObjectURL(new Blob([workerSource], { type: "text/javascript" }))
);
worker.onmessage = function (event) {
console.log("Received message from worker:", event.data);
};
worker.postMessage("Hello, worker!");
当然我们在 html 中也可以使用 script 标签来创建一个 Web Worker,例如:
<script id="worker" type="app/worker">
self.onmessage = function (event) {
console.log("Received message from main thread:", event.data);
self.postMessage("Hello, main thread!");
};
</script>
<script>
const objURL = URL.createObjectURL(
new Blob([document.querySelector("#worker").textContent])
);
const worker = new Worker(objURL);
worker.postMessage("Hello, worker!");
worker.onmessage = function (event) {
console.log("Received message from worker:", event.data);
};
</script>
Web Worker 的限制
-
无法访问 DOM
Worker 无法直接操作 DOM(如
document、window对象)。 -
无法访问全局变量
Worker 环境中的全局对象是
self,不是window,并且部分浏览器 API 不可用,例如:localStorage(但支持IndexedDB)。alert()、confirm()等界面交互方法。 -
参数传递限制
postMessage传递的数据必须是可序列化的,如果传递的参数是函数则直接报错,DOM 元素也是。 -
同源策略限制
Worker 脚本必须与主线程页面同源(协议、域名、端口完全一致),且禁止从本地文件加载 Worker,需通过服务器(如 localhost)运行。
-
资源消耗与管理
每个 Worker 占用独立线程和内存,创建过多 Worker 会导致系统资源耗尽,线程切换开销增加,反而降低性能。可以使用workerpool 线程池来管理线程。
-
请求
Web Worker 环境中没办法使用一些
axios或者jQuery的ajax,我们只能使用原生的fetch,或者使用XMLHttpRequest来解决该问题。