跳到主要内容

工作者线程

工作者线程

  • 基于线程实现;
  • 并行运行;
  • 可共享部分内存;
  • 不一定在同一个进程内;

工作者线程的类型

  • 专用工作者线程: 单独创建一个 js 线程执行任务;
  • 共享工作者线程 可以被多个上下文使用;
  • 服务工作者线程: 用于拦截, 重定向, 修改 HTTP 请求等;

WorkerGlobalScope

WorkerGlobalScope
  • 工作者线程中的全局对象;
  • 仅可用部分浏览器 API;
属性和方法
navigatorclearInterval()
selfclearTimeout()
locationcreateImageBitmap()
consolefetch()
cachessetInterval()
indexedDBsetTimeout()
isSecureContextatob()
originbtoa()
限制
  • DOM 限制: 无法读取主线程 DOM 对象;
    • Document, window, parent;
  • 全局对象限制: 只定义 Navigator 和 Location;
  • 脚本限制: 无法使用 alert() 和 confirm();

专用工作者线程

专用工作者线程

工作者线程 API
  • Worker(scriptURL: string, options?: WorkerOptions): Worker 构造函数: 创建专用工作者线程;
  • Worker.prototype.postMessage(message: any, transfer?: Transferable[]): void 实例方法: 异步发送信息到工作者线程;
  • Worker.prototype.terminate(): void 实例方法: 终止工作者线程, 立刻停止脚本;
  • Worker.prototype.onerror: ((this: Worker, ev: ErrorEvent) => void) | null 实例属性: 报错时触发;
  • Worker.prototype.onmessage: ((this: Worker, ev: MessageEvent) => void) | null 实例属性: 接收消息时触发;
  • Worker.prototype.onmessageerror: ((this: Worker, ev: MessageEvent) => void) | null 实例属性: 信息无法序列化时触发;
  • DedicatedWorkerGlobalScope.prototype.name: string 实例属性: 工作者线程标识符;
  • DedicatedWorkerGlobalScope.prototype.postMessage(message: any, transfer?: Transferable[]): void 实例方法: 发送消息到主线程;
  • DedicatedWorkerGlobalScope.prototype.close(): void 实例方法: 关闭工作者线程;
  • DedicatedWorkerGlobalScope.prototype.importScripts(...urls: string[]): void 实例方法: 导入外部脚本;
const sameOriginWorker = new Worker("./worker.js");
const remoteOriginWorker = new Worker("https://untrusted.com/worker.js");
close() vs terminate()
  • close(): 通知工作者线程取消下一个事件循环, 依旧执行当前事件循环;
  • terminate(): 立刻终止工作者线程, 强制中断当前事件循环;
// close() 示例
self.postMessage("foo");
self.close();
self.postMessage("bar"); // 当前事件循环中的任务依旧执行
setTimeout(() => self.postMessage("baz"), 0); // 被取消
// terminate() 示例
setTimeout(() => {
worker.postMessage("foo");
worker.terminate();
worker.postMessage("bar"); // 工作者线程消息队列清理并锁定, 该行无法执行
}, 1000);
同源策略
  • 只能加载同源 js 文件;
垃圾回收
  • 只要不使用 self.close() 和 worker.terminate();
  • 工作者线程不会被垃圾回收, 即使脚本执行完成;
处理工作者线程错误
// 工作者线程发生错误, 由于其为沙盒, 不会打断主线程执行;
// 主线程可通过 error 事件监听到工作者线程发生错误
// main.js
const worker = new Worker("./worker.js");
worker.onerror = console.log;
// ErrorEvent {message: "Uncaught Error: foo"}
// worker.js
throw Error("foo");

动态创建工作者线程

行内创建工作者线程

  • Blob + URL.createObjectURL();
const workerScript = `
self.onmessage = ({data}) => console.log(data);
`;
const workerScriptBlob = new Blob([workerScript]);
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
const worker = new Worker(workerScriptBlobUrl);
worker.postMessage("blob worker script");

工作者线程中动态执行脚本

  • 使用 importScripts() 动态导入脚本, 根据加载顺序同步执行;
importScripts()
// scriptA.js
console.log("scriptA executes");
// scriptB.js
console.log("scriptB executes");
// worker.js
console.log("importing scripts");
importScripts("./scriptA.js", "./scriptB.js");
console.log("scripts imported");
// importing scripts
// scriptA executes
// scriptB executes
// scripts imported
cors 限制和共享作用域
  • 工作者线程内部可使用任意源的 js 文件;
  • 导入的 js 文件共享工作者线程作用域;

与专用工作者通信

使用 postMessage()
// main.js
const worker = new Worker("./worker.js");
worker.onmessage = ({ data }) => console.log(data);
worker.postMessage(5);

// worker.js
self.onmessage = ({ data }) => {
self.postMessage(`${data} in worker.js`);
};
使用 MessageChannel
  • 使用 MessageChannel() API;
  • 可实现工作者线程相互通信/主线程与工作者线程通信;
// main.js
const channel = new MessageChannel();
const factorialWorker = new Worker("./worker.js");
factorialWorker.postMessage(null, [channel.port1]);
channel.port2.onmessage = ({ data }) => console.log(data);
channel.port2.postMessage(5);

// worker.js
self.onmessage = ({ ports }) => {
if (!messagePort) {
messagePort = ports[0];
self.onmessage = null;
messagePort.onmessage = ({ data }) => {
messagePort.postMessage(`${data} in worker.js`);
};
} else;
};
使用 BroadcastChannel
  • 使用 BroadcastChannel() API;
  • 限制同源脚本;
// main.js
const channel = new BroadcastChannel("worker_channel");
const worker = new Worker("./worker.js");
channel.onmessage = ({ data }) => {
console.log(`heard ${data} on page`);
};
setTimeout(() => channel.postMessage("foo"), 1000);

// worker.js
const channel = new BroadcastChannel("worker_channel");
channel.onmessage = ({ data }) => {
console.log(`heard ${data} in worker`);
channel.postMessage("bar");
};

工作者线程数据传输

结构化克隆算法
  • 通过 postMessage() 传递对象时使用结构化克隆算法生成副本;
  • 适用类型: 所有可序列化类型 (基本类型, 对象, 数组, Map, Set, Date, RegExp 等);
  • 性能开销较大, 应避免克隆过大对象;
  • 可设置 transferList 参数, 转移指定对象所有权, 原有上下文失去对齐引用;
// main.js
const worker = new Worker("./worker.js");
const arrayBuffer = new ArrayBuffer(32);
console.log(`page's buffer size: ${arrayBuffer.byteLength}`); // 32
worker.postMessage(arrayBuffer, [arrayBuffer]); // arrayBuffer 转移至工作者线程, 父上下文失去引用
console.log(`page's buffer size: ${arrayBuffer.byteLength}`); // 0
// worker.js
self.onmessage = ({ data }) => {
console.log(`worker's buffer size: ${data.byteLength}`); // 32
};
SharedArrayBuffer
  • 不同上下文共享同一个内存块的引用, 存在资源争用的风险, 使用 Atomics 对象解决;

共享工作者线程

  • 专用工作者线程的拓展;
  • 可以被多个同源上下文访问;
    • 不同窗口;
    • 不同标签页;
    • ...;
创建共享工作者线程
  • SharedWorker(scriptURL: string, options?: string | SharedWorkerOptions): SharedWorker 构造函数: 基于 js url 创建, 不存在创建实例, 存在共享实例, 可通过 name 强制区分;
  • SharedWorker.prototype.port: MessagePort 实例属性: 用于通信的 MessagePort;
  • SharedWorker.prototype.onerror: ((this: SharedWorker, ev: ErrorEvent) => void) | null 实例属性: 报错时触发;
  • SharedWorkerGlobalScope.prototype.name: string 实例属性: 工作者线程标识符;
  • SharedWorkerGlobalScope.prototype.importScripts(...urls: string[]): void 实例方法: 导入外部脚本;
  • SharedWorkerGlobalScope.prototype.close(): void 实例方法: 立即终止工作者线程;
  • SharedWorkerGlobalScope.prototype.onconnect: ((this: DedicatedWorkerGlobalScope, ev: MessageEvent) => void) | null 实例属性: 建立连接/传递信息时触发;
// 共享工作者线程
new SharedWorker("./sharedWorker.js");
new SharedWorker("https://www.example.com/sharedWorker.js");
// 强制标识
new SharedWorker("./sharedWorker.js", { name: "foo" });
new SharedWorker("./sharedWorker.js", { name: "bar" });

共享工作者线程的生命周期

生命周期
  • 初始化;
  • 活动;
  • 终止;
终止共享工作者线程
  • SharedWorker 对象无 terminate() 方法;
  • 调用 close() 方法只是终止该页面的连接;
  • 只要有一个端口连接;
  • 共享工作者线程不会终止;

连接到共享工作者线程

  • 调用 SharedWorker() 构造函数, 共享工作者线程触发 connect 事件;
  • 隐式创建 MessageChannel 实例, 保存至 connect 事件的 ports 数组;
// sharedWorker.js
const connectedPorts = new Set();
self.onconnect = ({ ports }) => {
connectedPorts.add(ports[0]);
console.log(`${connectedPorts.size} unique connected ports`);
};
// main.js
for (let i = 0; i < 5; ++i) {
new SharedWorker("./sharedWorker.js");
}