Web Worker
什么是Web Worker?
- Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。
- 此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。
- 一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)。
Web Worker 如何使用
一个 worker 是使用一个构造函数创建的一个对象运行一个命名的 JavaScript 文件 ———— 这个文件包含将在 worker 线程中运行的代码;
- worker 运行在另一个全局上下文中,不同于当前的window。因此,在 Worker 内通过 window 获取全局作用域(而不是self)将返回错误。
worker分为
专用 worker
和共享worker
:一个专用 worker 仅能被首次生成它的脚本使用,而共享 worker 可以同时被多个脚本使用。DedicatedWorkerGlobalScope 对象代表了专用 worker 上下文。
SharedWorkerGlobalScope 对象代表了共享 worker 上下文。
Web Workers和主线程通信
双方都使用 postMessage()
方法发送各自的消息。使用 onmessage
事件处理函数来响应消息(消息被包含在 message 事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。
只要运行在同源的父页面中,worker 可以依次生成新的 worker;并且可以使用 XMLHttpRequest 进行网络 I/O,但是 XMLHttpRequest 的 responseXML 和 channel 属性总会返回 null。
Web Workers 限制的操作
- 在 worker 内,不能直接操作 DOM 节点。
- 不能使用 window 对象的默认方法和属性。
因为不同于当前的window,worker 运行在另一个全局上下文中(上面也有提到)。
但是你可以使用大量 window 对象之下的东西,包括 WebSockets,以及 IndexedDB 等数据存储机制。
Web Workers可用函数
和类
上下文:
上下文 | 用于 |
---|---|
DedicatedWorkerGlobalScope | 专用 worker |
SharedWorkerGlobalScope | 共享 worker |
ServiceWorkerGlobalScope | service worker |
通用的函数:所有的 worker 和 主线程中均可用
WorkerGlobalScope.atob()
:编码WorkerGlobalScope.btoa()
:解码WorkerGlobalScope.createImageBitmap()
:图像处理WorkerGlobalScope.dump()
非标准WorkerGlobalScope.fetch()
:发起获取资源的请求WorkerGlobalScope.queueMicrotask()
:将微任务加入队列WorkerGlobalScope.reportError()
:异常处理WorkerGlobalScope.setInterval()
WorkerGlobalScope.setTimeout()
WorkerGlobalScope.clearInterval()
WorkerGlobalScope.clearTimeout()
WorkerGlobalScope.structuredClone()
:结构化克隆算法。
仅在 Worker 中可用的函数:
WorkerGlobalScope.importScripts()
:同步导入(所有的 worker)DedicatedWorkerGlobalScope.postMessage()
:发送消息(仅专用 worker)DedicatedWorkerGlobalScope.requestAnimationFrame()
:帧动画(仅专用 worker)DedicatedWorkerGlobalScope.cancelAnimationFrame()
:帧动画取消(仅专用 worker)
Worker 可用的 Web API:
Barcode Detection API
:检测图像中的条形码和二维码Broadcast Channel API
:同源的 worker 之间进行基本通信设置Cache API
:一些请求的缓冲设置Channel Messaging API
:同一个文档的不同浏览上下文通信Console API
:log 日志调试功能Web Crypto API:加密处理(例如
Crypto
)CSS Font Loading API:动态加载字体资源
CustomEvent
:初始化Encoding API
:各种字符编码文本处理(例如TextEncoder
、TextDecoder
)FileReader
:异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容FileReaderSync
:异步读取,因为它支持可能导致潜在的阻塞的同步 I/O。FormData
:格式化数据ImageBitmap
:位图图像ImageData
:隐含像素数据的区域IndexedDB
:客户端存储大量的结构化数据Media Source Extensions API:无插件且基于 Web 的流媒体的功能(仅限专用 worker)
Network Information API:网络类型的信息(如“wifi”、“cellular”等)
Notifications API
:允许网页控制向最终用户显示系统通知OffscreenCanvas
(和所有的 canvas context API)Performance API
:一组用于衡量 web 应用性能的标准Performance
:性能相关的信息PerformanceEntry
:performance 时间列表中的单个 metric 数据PerformanceMeasure
:添加命名的(度量)到浏览器的性能时间线。PerformanceMark
:添加命名的(标记)PerformanceObserver
:用于监测性能度量事件PerformanceResourceTiming
:检索和分析有关加载应用程序资源的详细网络计时数据。
Server-sent 事件:使用服务器发送事件
ServiceWorkerRegistration
:接口代表服务工作线程注册WebCodecs_API:为 web 开发者提供了对视频流的单个帧和音频数据块的底层访问能力
派生 Worker 可用的API:
worker 也可以派生其他 worker
专用Worker
一个专用 worker 仅能被生成它的脚本所使用。
创建、通信、终止:
// main.js
if(window.Worker){
// 创建
const worker = new Worker("worker.js");
// 发送
worker.postMessage('hello worker');
// 接收
worker.onmessage = function(e) {
console.log('from worker.js:', e.data);
// 终止
// worker.terminate();
};
}
// main.js
if(window.Worker){
// 创建
const worker = new Worker("worker.js");
// 发送
worker.postMessage('hello worker');
// 接收
worker.onmessage = function(e) {
console.log('from worker.js:', e.data);
// 终止
// worker.terminate();
};
}
// worker.js
// 接收
onmessage = (e) => {
console.log('from main.js:', e.data);
};
// 发送
postMessage(('hello worker').toUpperCase());
// worker.js
// 接收
onmessage = (e) => {
console.log('from main.js:', e.data);
};
// 发送
postMessage(('hello worker').toUpperCase());
注意:
- worker与主线程是并行。
- 同步代码先执行。
- postMessage是异步的。
错误处理:
错误事件有以下三个用户关心的字段:错误信息
、错误文件
、错误行号
// main.js
// 错误处理
worker.onerror = function(e) {
console.log('error',e.message); // 错误信息
console.log('error',e.filename); // 错误文件
console.log('error',e.lineno); // 错误行号
}
// main.js
// 错误处理
worker.onerror = function(e) {
console.log('error',e.message); // 错误信息
console.log('error',e.filename); // 错误文件
console.log('error',e.lineno); // 错误行号
}
生成 subworker:
如果需要的话,worker 能够生成更多的 worker,这就是所谓的 subworker,它们必须托管在同源的父页面内。而且,subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。 这使得 worker 更容易记录它们之间的依赖关系。
importScripts
引入脚本与库:
// worker.js
importScripts(); /* 什么都不引入 */
importScripts("foo.js"); /* 只引入 "foo.js" */
importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */
// worker.js
importScripts(); /* 什么都不引入 */
importScripts("foo.js"); /* 只引入 "foo.js" */
importScripts("foo.js", "bar.js"); /* 引入两个脚本 */
importScripts("//example.com/hello.js"); /* 你可以从其他来源导入脚本 */
如果脚本无法加载,将抛出 NETWORK_ERROR 异常,接下来的代码也无法执行。而之前执行的代码(包括使用 setTimeout() 异步执行的代码)依然能够运行。
importScripts() 之后的函数声明依然会被保留,因为它们始终会在其他代码之前运行。
备注:
脚本的下载顺序不固定,但执行时会按照传入 importScripts() 中的文件名顺序进行。这个过程是同步完成的;直到所有脚本都下载并运行完毕,importScripts() 才会返回。
当然web worker(type: 'module'
)中也是支持 import
特性 | importScripts | import |
---|---|---|
适用脚本类型 | 传统脚本(非模块) | ES模块 |
加载方式 | 同步,阻塞执行 | 异步,非阻塞 |
作用域 | 全局作用域 | 模块隔离作用域 |
兼容性 | 广泛支持 | 需较新浏览器及type: 'module' |
错误处理 | 触发Worker error 事件 | 支持try/catch 或Promise捕获 |
动态加载 | 不支持 | 支持动态加载import()函数 |
多文件加载 | 支持同时加载多个文件 | 需逐个导入或动态导入 |
共享Worker
构造函数名: SharedWorker
一个共享 worker 可以被多个脚本使用——即使这些脚本正在被不同的 window、iframe 或者 worker 访问。
一个非常大的区别在于,与一个共享 worker 通信 必须通过 port 对象 ————— 一个确切的打开的端口供脚本与 worker 通信(在专用 worker 中这一部分是隐式进行的)。
在传递消息之前,端口连接必须被显式的打开,打开方式是使用 onmessage 事件处理函数或者 start()
方法。只有一种情况下需要调用 start() 方法,那就是 message 事件被 addEventListener() 方法使用。
onmessage 等同 start() + addEventListener()
onconnect = function (e) {
const port = e.ports[0];
port.addEventListener("message", function (e) {
const workerResult = "Result: " + e.data[0] * e.data[1];
port.postMessage(workerResult);
});
port.start();
};
// 写法等同
onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (e) {
const workerResult = "Result: " + e.data[0] * e.data[1];
port.postMessage(workerResult);
};
};
onconnect = function (e) {
const port = e.ports[0];
port.addEventListener("message", function (e) {
const workerResult = "Result: " + e.data[0] * e.data[1];
port.postMessage(workerResult);
});
port.start();
};
// 写法等同
onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (e) {
const workerResult = "Result: " + e.data[0] * e.data[1];
port.postMessage(workerResult);
};
};
备注:
如果共享 worker 可以被多个浏览上下文调用,所有这些浏览上下文必须属于同源(相同的协议,主机和端口号)。
在 Firefox 中,共享 worker 不能被私有和非私有 window 对象的 document 所共享。
在使用 start() 方法打开端口连接时,如果父级线程和 worker 线程需要双向通信,那么它们都需要调用该方法 。
创建、通信、终止:
// A 页面
const sharedWorker = new SharedWorker("./shared-worker.js", "A");
sharedWorker.port.onmessage = (e) => {
console.log(e.data);
sharedWorker.port.close(); // 终止
};
sharedWorker.port.postMessage({type: 'add', params: [1,3,5,7,9]});
// A 页面
const sharedWorker = new SharedWorker("./shared-worker.js", "A");
sharedWorker.port.onmessage = (e) => {
console.log(e.data);
sharedWorker.port.close(); // 终止
};
sharedWorker.port.postMessage({type: 'add', params: [1,3,5,7,9]});
// B 页面
const sharedWorker = new SharedWorker("./shared-worker.js", "B");
sharedWorker.port.onmessage = (e) => {
console.log(e.data);
sharedWorker.port.close(); // 终止
};
sharedWorker.port.postMessage({ type: 'multiply', params: [2, 4] });
// B 页面
const sharedWorker = new SharedWorker("./shared-worker.js", "B");
sharedWorker.port.onmessage = (e) => {
console.log(e.data);
sharedWorker.port.close(); // 终止
};
sharedWorker.port.postMessage({ type: 'multiply', params: [2, 4] });
// shared-worker.js
onconnect = function (event) {
const port = event.ports[0];
port.onmessage = function (e) {
const result = compute(e.data.type, e.data.params);
port.postMessage(result)
};
};
function compute(type, value) {
const calculator = {
add: (value) => value.reduce((a, b) => a + b, 0),
subtract: (value) => value.reduce((a, b) => a - b),
multiply: (value) => value.reduce((a, b) => a * b, 1),
divide: (value) => value.reduce((a, b) => a / b),
}
return calculator[type](value)
}
// shared-worker.js
onconnect = function (event) {
const port = event.ports[0];
port.onmessage = function (e) {
const result = compute(e.data.type, e.data.params);
port.postMessage(result)
};
};
function compute(type, value) {
const calculator = {
add: (value) => value.reduce((a, b) => a + b, 0),
subtract: (value) => value.reduce((a, b) => a - b),
multiply: (value) => value.reduce((a, b) => a * b, 1),
divide: (value) => value.reduce((a, b) => a / b),
}
return calculator[type](value)
}
SharedWorker调试问题
SharedWorker是单独运行在worker线程中的,有自己的上下文,不能操作DOM,不能通过console和debugger查看,这些和AbstractWorker,ServiceWorker应该一样的。
ServiceWorker有webpack插件workbox,也有谷歌量身定做的调试工具。SharedWorker的调试却难倒了很多英雄汉。
其实 Chrome 浏览器也是有方法调试的:地址栏输入:
chrome://inspect/#workers
线程安全
Worker 接口会生成真正的操作系统级别的线程,如果你不太小心,那么并发会对你的代码产生有趣的影响。
然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。
内容安全策略
有别于创建它的 document 对象,worker 有它自己的执行上下文。因此普遍来说,worker 并不受限于创建它的 document(或者父级 worker)的内容安全策略。
举个例子,假设一个 document 有如下头部声明:
Content-Security-Policy: script-src 'self'
Content-Security-Policy: script-src 'self'
这个声明有一部分作用在于,禁止它内部包含的脚本代码使用 eval() 方法。然而,如果脚本代码创建了一个 worker,在 worker 上下文中执行的代码却是可以使用 eval() 的。
为了给 worker 指定内容安全策略,必须为发送 worker 代码的请求本身设置 Content-Security-Policy 响应标头。 (可在服务的配置完成)
有一个例外情况,即 worker 脚本的源如果是一个全局性的唯一的标识符(例如,它的 URL 协议为 data 或 blob),worker 则会继承创建它的 document 或者 worker 的 CSP。
worker 中数据的接收与发送
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。大部分浏览器使用结构化克隆来实现该特性。
结构化克隆算法可以接收 JSON 数据以及一些 JSON 不能表示的数据————比如循环引用。
示例:
- worker 到主页面之间传递的消息内容进行切换
- 可转移对象 ⭐⭐⭐
通过转让所有权(可转移对象)来传递数据。现代浏览器包含另一种性能更高的方法来将特定类型的对象传递给一个 worker 或从 worker 传回。————可转移对象
可转移对象从一个上下文转移到另一个上下文而不会经过任何拷贝操作。这意味着当传递大型数据集时会获得极大的性能提升。
// 创建一个 32MB 的“文件”,用从 0 到 255 的连续数值填充它——32MB = 1024 * 1024 * 32
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
// 创建一个 32MB 的“文件”,用从 0 到 255 的连续数值填充它——32MB = 1024 * 1024 * 32
const uInt8Array = new Uint8Array(1024 * 1024 * 32).map((v, i) => i);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
当你将一个 ArrayBuffer 对象从主应用转让到 Worker 中,原始的 ArrayBuffer 被清除并且无法使用。它包含的内容会(完整无差的)传递给 Worker 上下文。
获取更多该方法相关的可转让对象、性能及特性检测等方法————请参阅 HTML5 Rocks 中的Transferable Objects: Lightning Fast! 。
worker 常见的应用
- 在后台执行运算
worker 的一个优势在于能够执行处理器密集型的运算而不会阻塞 UI 线程。如:worker 用于计算斐波那契数。
- 划分任务给多个 worker
当多核系统流行开来,将复杂的运算任务分配给多个 worker 来运行已经变得十分有用,这些 worker 会在多处理器内核上运行这些任务。
嵌入式 worker
目前没有一种“官方”的方法能够像 <script>
元素一样将 worker 的代码嵌入到网页中。
但是如果一个 <script>
元素没有 src 属性,并且它的 type 属性没有指定成一个可运行的 MIME type,那么它就会被认为是一个数据块元素,并且能够被 JavaScript 使用。
“数据块”是 HTML5 中一个十分常见的特性,它可以携带几乎任何文本类型的数据。
结构化克隆算法
结构化克隆算法用于复制复杂 JavaScript 对象的算法。通过来自 Worker 的 postMessage()
或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环。
不持支
- Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
- 企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
- 对象的某些特定参数也不会被保留:
- RegExp 对象的 lastIndex 字段不会被保留。
- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
- 原形链上的属性也不会被追踪以及复制。
持支
JavaScript
类型:
Array
ArrayBuffer
Boolean
DataView
Date
Error
类型(仅限部分 Error 类型)。Map
Object
对象:仅限简单对象(如使用对象字面量创建的)。- 除
symbol
以外的基本类型。 RegExp
:lastIndex
字段不会被保留。Set
String
TypedArray
Error
类型:
- Error
- EvalError
- RangeError
- ReferenceError
- SyntaxError
- TypeError
- URIError(或其他会被设置为
Error
的)。
可转移对象
可转移的对象(Transferable object)是拥有属于自己的资源的对象,这些资源可以从一个上下文转移到另一个,确保资源一次仅在一个上下文可用。传输后,原始对象不再可用;它不再指向转移后的资源,并且任何读取或者写入该对象的尝试都将抛出异常。
可转移对象通常用于共享资源,该资源一次仅能安全地暴露在一个 JavaScript 线程中。例如,ArrayBuffer 是一个拥有内存块的可转移对象。当此类缓冲区(buffer)在线程之间传输时,相关联的内存资源将从原始的缓冲区分离出来,并且附加到新线程创建的缓冲区对象中。原始线程中的缓冲区对象不再可用,因为它不再拥有属于自己的内存资源了。
使用 structuredClone() 创建对象的深层拷贝时,也可以使用转移。克隆操作后,传输的资源将被移动到克隆的对象,而不是复制。
使用转移对象资源的机制取决于对象自身。例如,当 ArrayBuffer 在线程之间转移时,它指向的内存资源实际上以快速且高效的零拷贝操作在上下文之间移动。其他对象可以通过拷贝关联的资源,然后将它从旧的上下文中删除来转移它。
可转移对象支持的类型:
ArrayBuffer
MessagePort
ReadableStream
WritableStream
TransformStream
AudioData
ImageBitmap
VideoFrame
OffscreenCanvas
RTCDataChannel
TIP
在各自对象的兼容性信息中,如果拥有 transferable 子特性,浏览器的支持应该被展示(示例请参阅 RTCDataChannel)。在撰写本文时,并非所有可转移对象都已更新此信息。
可转移的对象在 Web IDL 文件中用属性 [Transferable] 标记。
示例:
- 在线程之间传输对象。
以下代码演示了当消息从主线程发送到 web worker 线程时,传输是如何工作的。Uint8Array 在其缓冲区被转移时,被拷贝到 worker 中。传输后,任何尝试从主线程读或者写 uInt8Array 都将抛出错误,但是你仍然可以检查 byteLength 以确定它现在是 0。
// Create an 8MB "file" and fill it. 8MB = 1024 * 1024 * 8 B
const uInt8Array = new Uint8Array(1024 * 1024 * 8).map((v, i) => i);
console.log(uInt8Array.byteLength); // 8388608
// Transfer the underlying buffer to a worker
worker.postMessage(uInt8Array, [uInt8Array.buffer]);
console.log(uInt8Array.byteLength); // 0
// Create an 8MB "file" and fill it. 8MB = 1024 * 1024 * 8 B
const uInt8Array = new Uint8Array(1024 * 1024 * 8).map((v, i) => i);
console.log(uInt8Array.byteLength); // 8388608
// Transfer the underlying buffer to a worker
worker.postMessage(uInt8Array, [uInt8Array.buffer]);
console.log(uInt8Array.byteLength); // 0
- 在进行克隆操作时转移。
以下代码展示了 structuredClone() 操作,将底层缓冲区从原始对象复制到克隆对象(clone)。
const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024
original[0] = 1;
console.log(clone[0]); // 0
// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1
// After transferring Uint8Array.buffer cannot be used.
console.log(original.byteLength); // 0
const original = new Uint8Array(1024);
const clone = structuredClone(original);
console.log(original.byteLength); // 1024
console.log(clone.byteLength); // 1024
original[0] = 1;
console.log(clone[0]); // 0
// We can transfer Uint8Array.buffer.
const transferred = structuredClone(original, { transfer: [original.buffer] });
console.log(transferred.byteLength); // 1024
console.log(transferred[0]); // 1
// After transferring Uint8Array.buffer cannot be used.
console.log(original.byteLength); // 0