工作者线程
基础
工作者线程与线程
工作者线程与线程
- 工作者线程通过线程实现;
- 工作者线程并行运行;
- 工作者线程可共享部分内存;
- 工作者线程不一定在同一个进程内;
工作者线程的类型
专用工作者线程
- 单独创建一个 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