跳到主要内容

工作者线程

基础

工作者线程与线程

工作者线程与线程
  • 工作者线程通过线程实现;
  • 工作者线程并行运行;
  • 工作者线程可共享部分内存;
  • 工作者线程不一定在同一个进程内;

工作者线程的类型

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

WorkerGlobalScope

WorkerGlobalScope
  • 工作者线程中的全局对象;
属性和方法
  • navigator;
  • self;
  • location;
  • console;
  • caches;
  • indexedDB;
  • isSecureContext:表示工作者线程上下文是否安全;
  • origin;
  • atob();
  • btoa();
  • clearInterval();
  • clearTimeout();
  • createImageBitmap();
  • fetch();
  • setInterval();
  • setTimeout();
限制
  • DOM 限制:无法读取主线程 DOM 对象;
    • Document,window,parent;
  • 全局对象限制:只定义 Navigator 和 Location;
  • 脚本限制:无法使用 alert() 和 confirm();
子类
  • 继承于 WorkerGlobalScope;
  • 专用工作者线程:DedicatedWorkerGlobalScope;
  • 共享工作者线程:SharedWorkerGlobalScope;
  • 服务工作者线程:ServiceWorkerGlobalScope;

专用工作者线程

基本概念

创建工作者线程
// 传递 js 文件
const worker = new Worker(location.href + "emptyWorker.js");
console.log(worker); // Worker {}
安全限制
  • 智能能加载同源 js 文件;
const sameOriginWorker = new Worker("./worker.js");

const remoteOriginWorker = new Worker("https://untrusted.com/worker.js");
// Error: Uncaught DOMException: Failed to construct 'Worker':
// Script at https://untrusted.com/main.js cannot be accessed
// from origin https://example.com
使用 Work 对象
  • 事件;
    • error:报错触发;;
    • message:发送信息触发;
    • messageerror:信息无法序列化触发;
  • 方法;
    • postMessage():异步发送信息;
    • terminate():终止工作者线程,立刻停止脚本;
DedicatedWorkerGlobalScope
  • 工作者线程可以通过 self 获取自身的全局作用域;
// globalScopeWorker.js
console.log("inside worker:", self);
// main.js
const worker = new Worker("./globalScopeWorker.js");
console.log("created worker:", worker);
// created worker: Worker {}
// inside worker: DedicatedWorkerGlobalScope {}

// 特有属性和方法
self.name; // 标识符
self.postMessage(); // 对应 worker.postMessage()
self.close(); // 对应 worker.terminate()
self.importScripts(); // 导入 js 脚本
生命周期
  • 初始化;
  • 活动;
  • 终止;
垃圾回收
  • 只要不使用 self.close() 和 worker.terminate();
  • 工作者线程不会被垃圾回收,即使脚本执行完成;
close() 的执行时机
  • close()在这里会通知工作者线程取消下一个事件循环中的所有任务;
  • 但是当前事件循环中的任务依旧执行;
// closeWorker.js
self.postMessage("foo");
self.close();
self.postMessage("bar"); // 当前事件循环中的任务依旧执行
setTimeout(() => self.postMessage("baz"), 0); // 被取消

// main.js
const worker = new Worker("./closeWorker.js");
worker.onmessage = ({ data }) => console.log(data);
// foo
// bar
terminate() 的执行时机
  • terminate() 立刻终止工作者线程;
// terminateWorker.js
self.onmessage = ({ data }) => console.log(data);

//main.js
const worker = new Worker("./terminateWorker.js");
// 给 1000 毫秒让工作者线程初始化
setTimeout(() => {
worker.postMessage("foo");
worker.terminate();
worker.postMessage("bar"); // 工作者线程消息队列清理并锁定, 该行无法执行
setTimeout(() => worker.postMessage("baz"), 0);
}, 1000);
// foo
处理工作者线程错误
// 工作者线程发生错误, 由于其为沙盒, 不会打断主线程执行;
// 主线程可通过 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 行内脚本创建
// 创建要执行的 JavaScript 代码字符串
const workerScript = `
self.onmessage = ({data}) => console.log(data);
`;
// 基于脚本字符串生成 Blob 对象
const workerScriptBlob = new Blob([workerScript]);
// 基于 Blob 实例创建对象 URL
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob);
// 基于对象 URL 创建专用工作者线程
const worker = new Worker(workerScriptBlobUrl);
worker.postMessage("blob worker script");
// blob worker script

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

importScripts()
// 根据加载顺序同步执行
const worker = new Worker("./worker.js");
// importing scripts
// scriptA executes
// scriptB executes
// scripts imported

// 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");
cors 限制和共享作用域
// 工作者线程内部可使用任意源的 js 文件
// 导入的 js 文件共享工作者线程作用域
// main.js
const worker = new Worker("./worker.js", { name: "foo" });
// importing scripts in foo with bar
// scriptA executes in foo with bar
// scriptB executes in foo with bar
// scripts imported

// scriptA.js
console.log(`scriptA executes in ${self.name} with ${globalToken}`);
// scriptB.js
console.log(`scriptB executes in ${self.name} with ${globalToken}`);
// worker.js
const globalToken = "bar";
console.log(`importing scripts in ${self.name} with ${globalToken}`);
importScripts("./scriptA.js", "./scriptB.js");
console.log("scripts imported");

与专用工作者通信

使用 postMessage()
// 异步通信
// factorialWorker.js
function factorial(n) {
let result = 1;
while (n) {
result *= n--;
}
return result;
}
self.onmessage = ({ data }) => {
self.postMessage(`${data}! = ${factorial(data)}`);
};
// main.js
const factorialWorker = new Worker("./factorialWorker.js");
factorialWorker.onmessage = ({ data }) => console.log(data);
factorialWorker.postMessage(5);
factorialWorker.postMessage(7);
factorialWorker.postMessage(10);
// 5! = 120
// 7! = 5040
// 10! = 3628800
使用 MessageChannel
  • MessageChannel 实例具有两个端口 port1 和 port2,代表两个通信端点,需要传递一个至工作者线程;
// worker.js
// 在监听器中存储全局 messagePort
let messagePort = null;
function factorial(n) {
let result = 1;
while (n) {
result *= n--;
}
return result;
}

self.onmessage = ({ ports }) => {
if (!messagePort) {
messagePort = ports[0];
self.onmessage = null;
messagePort.onmessage = ({ data }) => {
messagePort.postMessage(`${data}! = ${factorial(data)}`);
};
} else;
};
// 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);
// 5! = 120
使用 MessageChannel 实现工作者线程相互通信
  • 两个端口传递给两个工作者线程;
const channel = new MessageChannel();
const workerA = new Worker("./worker.js");
const workerB = new Worker("./worker.js");
workerA.postMessage("workerA", [channel.port1]);
workerB.postMessage("workerB", [channel.port2]);
workerA.onmessage = ({ data }) => console.log(data);
workerB.onmessage = ({ data }) => console.log(data);
workerA.postMessage(["page"]);
// ['page', 'workerA', 'workerB']
workerB.postMessage(["page"]);
// ['page', 'workerB', 'workerA']
使用 BroadcastChannel
  • 同源脚本使用;
// 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);
// heard foo in worker
// heard bar on page

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

工作者线程数据传输

结构化克隆算法
  • 通过 postMessage() 传递对象,生成一个副本;
  • 适用类型:基本所有;
  • 性能开销较大,避免克隆过大对象;
可转移对象
  • 将所有权从一个上下文传递给另一个上下文;
    • postMessage 第二个参数为数组,指定转移对象;
    • worker 的 message 事件中接受一个可转移对象作为属性的 object;
  • 原本上下文会失去对可转移对象的内存引用;
  • 适用对象;
    • ArrayBuffer;
    • MessagePort;
    • ImageBitmap;
    • OffscreenCanvas;
// main.js
const worker = new Worker("./worker.js");
// 创建 32 位缓冲区
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 对象解决;
// main.js
const worker = new Worker("./worker.js");
const sharedArrayBuffer = new SharedArrayBuffer(1);
const view = new Uint8Array(sharedArrayBuffer);
view[0] = 1;
worker.onmessage = () => {
console.log(`buffer value after worker modification: ${view[0]}`);
};
worker.postMessage(sharedArrayBuffer);
// buffer value before worker modification: 1
// buffer value after worker modification: 2

// worker.js
self.onmessage = ({ data }) => {
const view = new Uint8Array(data);
console.log(`buffer value before worker modification: ${view[0]}`);
view[0] += 1;
self.postMessage(null);
};

共享工作者线程

基础

工作者线程
  • 专用工作者线程的拓展;
  • 可以被多个同源上下文访问;
    • 不同窗口;
    • 不同标签页;
    • 。。。;
创建共享工作者线程
const sharedWorker = new SharedWorker("emptySharedWorker.js");
console.log(sharedWorker); // SharedWorker {}
SharedWorker 标识与独占
  • SharedWorker() 只会在 SharedWorker 标识不存在时创建实例;
  • 否则连接已有实例;
// 共享一个共享工作者线程
new SharedWorker("./sharedWorker.js");
new SharedWorker("sharedWorker.js");
new SharedWorker("https://www.example.com/sharedWorker.js");

// 强制标识
new SharedWorker("./sharedWorker.js", { name: "foo" });
new SharedWorker("./sharedWorker.js", { name: "foo" });
new SharedWorker("./sharedWorker.js", { name: "bar" });
SharedWorkerGlobalScope
属性/方法作用
name标识符
importScripts()导入脚本
close()立即终止工作者线程
onconnect建立连接/传递信息触发
打印至控制台
  • 在 SharedWorker 中把日志打印到控制台不一定能在浏览器默认的控制台中看到;

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

生命周期
  • 初始化;
  • 活动;
  • 终止;
终止共享工作者线程
  • 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");
}
// 1 unique connected ports
// 2 unique connected ports
// 3 unique connected ports
// 4 unique connected ports
// 5 unique connected ports