什么是 Web Worker
Web Worker 是一种在浏览器并行运行 JavaScript 的机制,这允许我们在不阻塞主线程的情况下执行复杂和耗时的任务。
我们都知道 JavaScript 本身是单线程的,但浏览器通过 Web Worker API 提供了一种机制,使得开发者可以在独立的线程中运行 JavaScript 代码,从而实现并行处理。
Web Worker 的主要特点:
- 无阻塞:Web Worker 在独立于主线程的其它线程中运行,主线程可以继续处理用户交互和界面更新,当其它线程的任务处理完成后会通知主线程。
- 独立作用域:Web Worker 有自己独立的作用域,不能直接访问主线程中的 DOM 元素。
- 消息传递:主线程和 Web Worker 之间通过消息传递进行通信,使用
postMessage方法发送消息,使用onmessage事件接收消息。 - 安全性:Web Worker 运行在一个受限的环境中,不能访问某些全局对象(如
window、document),以确保安全性。
Web Worker 的使用场景
-
需要执行大量计算,例如:
- 图像处理:如图像滤镜、图像转换等。
- 数学计算:如大数运算、复杂算法等。
- 音视频解码
-
处理大批量数据,例如:
- 文件处理:如读取和解析大文件、文件压缩和解压缩、加密等
- 数据解析:如解析大型 JSON 数据、CSV 文件等。
- 数据转换:如数据格式转换、数据清洗等。
-
处理耗时的网络请求,例如:
- 批量请求:如批量获取数据。
- 长时间请求:需要长时间等待的 API 请求。
-
3D 或者地图场景。例如:
- 复杂场景的渲染和复杂的坐标计算
Web Worker 的具体用法
创建 Web Worker
Web Worker 的构造函数接受两个参数:
const worker = new Worker('xxx.js', { type: 'module', credentials: 'omit' });
-
必填的 worker 脚本 URL
- 类型:
string - 描述: 指向 worker 脚本的 URL。这个 URL 必须是同源的,或者服务器必须支持 CORS。
- 类型:
-
可选的配置对象,用于指定一些额外的选项。
type: 指定 worker 的类型,可以是
classic或module,默认值是module。-
classic代表使用传统的 JavaScript 文件作为 Worker 脚本,不支持 ES6 模块语法(如import和export)。 -
module使用 ES6 模块语法的 JavaScript 文件作为 Worker 脚本。
credentials: 决定了在 Worker 中发起的网络请求(如
fetch请求)是否会携带凭据(如 cookies、HTTP 认证信息等),仅在type为'module'时有效。可以是omit、same-origin或include。默认值是omit。omit不发送凭据(如 cookies 和 HTTP 认证信息)same-origin仅在同源请求中发送凭据include在所有请求中发送凭据,不论是否同源
-
启动任务,传递参数,异常处理
主线程通过 worker.postMessage(); 传递参数,并且其启动任务。
main.ts
worker.onmessage = (event) : void => {
console.log('Received from worker:', event.data);
};
worker.onerror = (error) : void => {
console.log('Error from worker:', error);
};
// Send data to the worker and start worker
worker.postMessage(10);
worker 通过 addEventListener('message', (data)=>{}) 接收。
worker.ts
addEventListener('message', ({ data }) => {
console.log(data);
// log 10
});
怎么关闭 Worker
在 Web Worker 中,可以通过以下两种方式关闭 Worker:
- 从主线程关闭 Worker:使用
terminate方法。 - 从 Worker 内部关闭 Worker:使用
self.close方法。
主线程:
worker.terminate();
worker 内部:
self.close();
在 Worker 中怎么引入外部依赖库文件
-
使用 ES6 模块。
这种方法允许你使用
import和export语法来组织代码,并且可以利用模块化的优势,如按需加载和作用域隔离。// main const worker = new Worker(xxxx, { type: 'module' }); worker.onmessage = function(event) { console.log('Received from worker:', event.data); }; worker.postMessage('Hello, Worker!'); // worker import { someFunction } from './utils'; onmessage = function(event) { const result = someFunction(event.data); postMessage(result); }; // utils export function someFunction(data): string { return `Processed: ${data}`; } -
使用传统的
importScripts它可以在 Worker 中引入一个或多个脚本文件,并立即执行这些脚本。
// main const worker = new Worker(xxxx, { type: 'classic' }); // worker importScripts('utils.js', 'another-script.js'); onmessage = function(event) { const result = someFunction(event.data); postMessage(result); };
使用 Worker 对文件进行分片以及生成 md5 的例子
file.worker.ts
/// <reference lib="webworker" />
import SparkMD5 from 'spark-md5';
import { FileChunkInfo } from './upload';
addEventListener('message', ({ data }) => {
const { file, chunkSize } = data as {
file: File;
chunkSize: number;
};
const totalChunks = Math.ceil(file.size / chunkSize);
const chunks: Array<FileChunkInfo> = [];
const fileReader = new FileReader();
const spark = new SparkMD5.ArrayBuffer();
let currentChunkIndex = 0;
fileReader.onload = (event: ProgressEvent<FileReader>) => {
const arrayBuffer = event.target?.result as ArrayBuffer;
const chunkMd5 = SparkMD5.ArrayBuffer.hash(arrayBuffer);
const blob = file.slice(
currentChunkIndex * chunkSize,
(currentChunkIndex + 1) * chunkSize
);
chunks.push({
md5: chunkMd5,
blob,
index: currentChunkIndex,
id: `${chunkMd5}-${currentChunkIndex}`,
});
spark.append(arrayBuffer);
currentChunkIndex++;
if (currentChunkIndex < totalChunks) {
loadNextChunk();
} else {
const fileMd5 = spark.end();
// 任务处理完成,通知主线程
postMessage({ chunks, fileMd5 });
}
};
const loadNextChunk = () => {
const start = currentChunkIndex * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const blob = file.slice(start, end);
fileReader.readAsArrayBuffer(blob);
};
loadNextChunk();
});
在主线程中使用 Worker:
const worker = new Worker(new URL('./file.worker', import.meta.url));
worker.postMessage({ file: selectedFile, chunkSize: 1024 * 1024 });
worker.onmessage = (event) => {
console.log('Chunks:', event.data);
};
扩展知识点
Angular中如何使用 WebWorker
来自于同一个 URL 的 Web Worker 会创建多个 Worker 实例吗?
是的,每次调用 new Worker() 都会创建一个新的 Worker 实例,即使 URL 相同。这些实例是独立的,互不干扰。
Worker 执行完后,它内部声明的变量会自动被 GC 处理掉吗?
是的,Worker 执行完后,如果没有其他引用指向它内部声明的变量,这些变量就会被垃圾回收机制自动处理掉。
Worker 在创建出来后会立马占用 CPU 核心资源吗?
在执行了 new Worker 后,浏览器会浏览器立即创建一个新的线程,并开始执行 worker.js 脚本,但通常只有在主线程发送事件,并启动任务时才会占用更多的 CPU 资源,空闲的 Worker 占用的资源较少。
可以动态地创建 Worker 吗?
有时候我们可能希望不同的任务创建不同的 Worker示例,通过封装一个类,接收 Worker 构造函数需要的参数动态的创建 Worker。
但是当你尝试后会发现,在 new Worker 实例时,如果动态传递 URL 给 Worker的构造函数,
const url = new URL('./file.worker', import.meta.url);
const worker = new Worker(new URL('./file.worker', import.meta.url));
在运行时你会得到下面的错误:
Uncaught SyntaxError: Unexpected token '<'
我们可以通过编译后的产物来分析下原因:
正常的编译产物,是包含 worker-2MG6XPRU.js(worker文件编译后的产物)的。
而使用参数传递 URL 的编译产物
你会发现在使用 url 传参后,worker 文件根本就没有被编译,编译器认为 worker文件没有被使用。
调用时的对比图:
正常使用时,Worker 原始的文件名被替换为编译后的文件名,而在使用URL变量的时候,编译后保留了原始的文件名,但此时显然是无法访问这个文件的。
其实不允许通过变量传递 Worker 文件,主要是因为 Worker 构造函数需要一个静态的、在编译时可解析的 URL。
-
在
JavaScript中,new URL是一个构造函数,用于解析和构建URL。import.meta.url提供了当前模块的URL。将这两个结合起来,可以生成一个相对于当前模块的URL。 -
编译器必须在编译时知道
Worker的URL,以便正确地处理模块依赖关系。当你尝试将new URL的结果存储在一个变量中,然后再传递给Worker构造函数时,编译器无法在编译时解析这个变量。
如果你一定要动态创建 Worker, 可以通过创建 js 链接的方式,下面是一个示例:
const workerCode = `
self.onmessage = function(event) {
const data = event.data;
// Perform some work here
const result = data; // Replace with actual work result
self.postMessage(result);
};
`;
dynamicCreateWorker(workerCode);
private dynamicCreateWorker(workerCode: string): Worker {
const blob = new Blob([workerCode], { type: 'application/javascript' });
const blobURL = URL.createObjectURL(blob);
const worker = new Worker(blobURL);
worker.onmessage = (event) => this.onWorkerMessage(worker, event);
worker.onerror = (error) => this.onWorkerError(worker, error);
return worker;
}
创建出来的 Worker 有数量限制吗?如果有,要怎么解决?
浏览器对 Worker 的数量有一定限制,具体取决于浏览器和系统资源。超过限制可能会导致性能问题或无法创建新的 Worker。
解决方法:
使用类似线程池的概念来管理 Worker,根据硬件资源,限制 Worker的数量,我们可以通过 navigator.hardwareConcurrency 获取当前系统的 cpu 核心数。然后通过传递 createWorker 函数,来动态的创建 Worker。
import { Injectable } from '@angular/core';
export interface WorkerTask {
params: any;
createWorker: () => Worker;
resolve: Function;
reject: Function;
}
@Injectable({
providedIn: 'root',
})
export class WorkerPoolService {
private readonly MAX_WORKERS: number;
private taskQueue: Array<WorkerTask> = [];
private runningTasks: Map<Worker, { resolve: Function; reject: Function }> =
new Map();
constructor() {
// Fallback to 4 if hardwareConcurrency is not available
const cpuCores = navigator.hardwareConcurrency || 4;
this.MAX_WORKERS = Math.max(1, cpuCores - 2);
}
runTask<T>(createWorker: () => Worker, params?: any): Promise<T> {
return new Promise((resolve, reject) => {
this.taskQueue.push({
params,
createWorker,
resolve,
reject,
});
this.processNextTask();
});
}
private processNextTask(): void {
if (this.taskQueue.length === 0) {
return;
}
if (this.runningTasks.size < this.MAX_WORKERS) {
const task = this.taskQueue.shift()!;
if (task) {
const { createWorker, params, resolve, reject } = task;
const worker = createWorker();
worker.onmessage = (event) => this.onWorkerMessage(worker, event);
worker.onerror = (error) => this.onWorkerError(worker, error);
this.runningTasks.set(worker, {
resolve,
reject,
});
worker.postMessage(params);
}
}
}
private onWorkerMessage(worker: Worker, event: MessageEvent): void {
const { resolve } = this.runningTasks.get(worker)!;
resolve(event.data);
this.cleanupWorker(worker);
this.processNextTask();
}
private onWorkerError(worker: Worker, error: ErrorEvent): void {
const { reject } = this.runningTasks.get(worker)!;
reject(error);
this.cleanupWorker(worker);
this.processNextTask();
}
private cleanupWorker(worker: Worker): void {
this.runningTasks.delete(worker);
worker.terminate();
}
}
SharedWorker 和 ServiceWorker
Shared Worker 允许多个脚本(即多个浏览器上下文,如不同的浏览器标签页、iframe 等)共享同一个 Worker 实例。它适用于需要在多个浏览器上下文之间共享数据的场景。
特点:
- 共享线程:多个浏览器上下文可以共享同一个 Worker 实例。
- 通信:通过
postMessage和onmessage进行通信,并使用MessagePort进行连接。 - 无 DOM 访问:不能直接访问 DOM。
// main
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function(event) {
console.log('Received from shared worker:', event.data);
};
worker.port.postMessage('Hello, Shared Worker!');
// shared-worker
const workers = [];
self.addEventListener('connect', event => {
const port = event.ports[0];
// 遍历所有已连接的 port,发送消息
workers.forEach(port => {
port.postMessage(xxx);
})
port.start();
// 存储已连接的port
workers.push(port);
});
Service Worker 是一种特殊类型的 Worker,主要用于拦截和处理网络请求,提供离线缓存功能。它适用于需要增强 Web 应用的离线体验和性能的场景。比较知名的 Mock 库 MSW 就是借助了 ServiceWorker。
特点:
- 拦截网络请求:可以拦截和处理网络请求,提供离线缓存。
- 生命周期:Service Worker 有独特的生命周期,包括安装、激活和运行阶段。
- 无 DOM 访问:不能直接访问 DOM。