Web Workers API
1、指南
1.1 使用Web Workers
Web Workers是一种让Web内容在后台线程中运行脚本的简单方法。工作线程可以在不干扰用户界面的情况下执行任务。此外,它们还可以使用XMLHttpRequest
(尽管responseXML
和channel
属性总是为空)或fetch
(没有此类限制)执行I/O。工作线程一旦被创建,就可以通过向JavaScript代码指定的事件处理程序发送消息来向创建它的JavaScript代码发送消息(反之亦然)。
本文详细介绍了如何使用web worker。
1.1.1 Dedicated workers
如上所述,Dedicated worker只能由调用它的脚本访问。在本节中,我们将讨论在 Basic dedicated worker example示例中发现的JavaScript:它允许您输入两个要相乘的数字。这些数字被发送给一个专门的工作者,相乘,结果返回到页面并显示。
这个示例相当简单,但我们决定在向您介绍基本的worker概念时保持简单。本文稍后将介绍更高级的细节。
Worker feature detection
为了更好地控制错误处理和向后兼容性,将 worker 访问代码包装在下面(main.js)中是一个好主意:
if (window.Worker) {
// …
}
Spawning a dedicated worker
创建一个新的worker很简单。您所需要做的就是调用Worker()
构造函数,指定要在Worker
线程中执行的脚本的URI
。
// main.js
const myWorker = new Worker("worker.js");
向专用worker发送和从专用worker发送消息
worker的魔力是通过postMessage()
方法和onmessage
事件处理程序实现的。当您想要向worker发送消息时,您可以像这样向它发送消息
first.onchange = () => {
myWorker.postMessage([first.value, second.value]);
console.log("Message posted to worker");
};
second.onchange = () => {
myWorker.postMessage([first.value, second.value]);
console.log("Message posted to worker");
};
这里有两个<input>
元素分别由变量first
和second
表示;当其中一个的值被改变时,myWorker.postMessage([first.value,second.value])
被用来将两者中的值以数组的形式发送给worker。你可以在消息中发送任何你喜欢的东西。
在worker中,当收到消息时,我们可以通过编写这样的事件处理程序块来响应:
// worker.js
onmessage = (e) => {
console.log("Message received from main script");
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
console.log("Posting message back to main script");
postMessage(workerResult);
};
onmessage
处理程序允许我们在接收到消息时运行一些代码,消息本身在message
事件的data
属性中可用。这里我们将两个数字相乘,然后再次使用postMessage(),将结果发送回主线程。
回到主线程,我们再次使用onmessage
来响应从worker发送回来的消息:
myWorker.onmessage = (e) => {
result.textContent = e.data;
console.log("Message received from worker");
};
这里我们获取消息事件数据并将其设置为result
段落的textContent
,这样用户就可以看到计算的结果。
结束 worker
如果你需要在主线程中立即终止一个正在运行的worker,你可以通过调用worker的terminate
方法来完成:
myWorker.terminate();
工作线程被立即终止。
处理错误
当工作线程中发生运行时错误时,将调用其onerror
事件处理程序。它接收一个名为error
的事件,该事件实现了ErrorEvent
接口。
事件不会冒泡并且可以取消;为了防止默认操作发生,worker可以调用错误事件的preventDefault()
方法。
错误事件有以下三个感兴趣的字段:
- message
一个人类可读的错误信息。 - filename
发生错误的脚本文件的名称。 - lineno
发生错误的脚本文件的行号。
Spawning subworkers
如果workers 愿意,他们可能会产生更多的workers 。所谓的子worker必须托管在与父页面相同的源中。此外,子worker的uri是相对于父worker的位置而不是所属页面的位置进行解析的。这使得workers 更容易跟踪他们的依赖项在哪里。
导入脚本和库
工作线程可以访问全局函数importScripts()
,该函数允许它们导入脚本。它接受零个或多个uri作为要导入的资源的参数;以下所有例子都是正确的:
importScripts(); /* imports nothing */
importScripts("foo.js"); /* imports just "foo.js" */
importScripts("foo.js", "bar.js"); /* imports two scripts */
importScripts(
"//example.com/hello.js",
); /* You can import scripts from other origins */
浏览器加载列出的每个脚本并执行它。然后,worker可以使用每个脚本中的任何全局对象。如果无法加载脚本,则抛出NETWORK_ERROR
,并且不会执行后续代码。之前执行的代码(包括使用setTimeout()
延迟的代码)仍然可以使用。还保留了importScripts()
方法之后的函数声明,因为它们总是在其余代码之前求值。
1.1.2 Shared workers
一个共享的工作线程可以被多个脚本访问——即使它们被不同的窗口、iframe甚至工作线程访问。在本节中,我们将讨论基 Basic shared worker example示例中的JavaScript(运行shared worker):这与basic dedicated worker 示例非常相似,除了它有两个可用的函数,由不同的脚本文件处理:两个数字的乘法,或一个数字的平方。两个脚本都使用同一个worker来执行所需的实际计算。
在这里,我们将集中讨论 dedicated workers 和shared workers之间的区别。请注意,在这个示例中,我们有两个HTML页面,每个页面都应用了使用相同工作文件的JavaScript。
生成一个 shared worker
生成一个新的共享工作线程与 dedicated 工作线程非常相似,但使用不同的构造函数名称(参见index.html
和index2.html
) -每个构造函数都必须使用如下代码启动工作线程:
const myWorker = new SharedWorker("worker.js");
一个很大的区别是,对于共享工作线程,您必须通过port
对象进行通信—打开一个显式端口,脚本可以使用该端口与工作线程进行通信(这在 dedicated 工作线程的情况下隐式完成)。
端口连接需要通过使用onmessage
事件处理程序隐式启动,或者在发布任何消息之前显式地使用start()
方法启动。只有当消息事件通过addEventListener()
方法连接时才需要调用start()
。
注意:当使用start()方法打开端口连接时,如果需要双向通信,则需要从父线程和工作线程都调用该方法。
向共享worker发送消息和从共享worker发送消息
现在消息可以像以前一样发送到worker,但是postMessage()
方法必须通过port
对象调用(同样,你会在 multiply.js
和square.js
中看到类似的结构):
// square.js/multiply.js
squareNumber.onchange = () => {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log("Message posted to worker");
};
现在,来看看worker。这里也有一点复杂
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = (e) => {
const workerResult = `Result: ${e.data[0] * e.data[1]}`;
port.postMessage(workerResult);
};
};
首先,当端口连接发生时,我们使用onconnect
处理程序来触发代码(例如,当父线程中的onmessage
事件处理程序被设置时,或者当父线程中显式调用start()
方法时)。
我们使用该事件对象的ports
属性来获取端口并将其存储在一个变量中。
接下来,我们在端口上添加一个onmessage
处理程序来执行计算并将结果返回给主线程。在工作线程中设置这个onmessage
处理程序也会隐式地打开到父线程的端口连接,因此实际上不需要调用port.start()
,如上所述。
最后,回到主脚本,我们处理消息(同样,您将在multi .js和square.js中看到类似的结构)。
// multiply.js and square.js
myWorker.port.onmessage = (e) => {
result2.textContent = e.data;
console.log("Message received from worker");
};
当消息通过端口从worker返回时,我们将计算结果插入到适当的结果段落中。
1.2 Web worker可用的函数和类
除了标准的JavaScript函数集(如String
、Array
、Object
、JSON
等)之外,还有各种各样的函数可以从DOM中提供给worker。本文提供了一个列表。
Worker Contexts & Functions
工作线程运行在与当前窗口(window)不同的全局上下文中!虽然Window不是直接对worker可用,但许多相同的方法都是在一个共享的mixin (WindowOrWorkerGlobalScope
)中定义的,并且通过worker自己的WorkerGlobalScope
派生的上下文提供给worker使用:
DedicatedWorkerGlobalScope
for dedicated workersSharedWorkerGlobalScope
for shared workersServiceWorkerGlobalScope
for service workers
一些函数(一个子集)是所有工作线程和主线程(来自WindowOrWorkerGlobalScope
)共同的:
- atob()
- btoa()
- clearInterval()
- clearTimeout()
- dump() Non-standard
- queueMicrotask()
- setInterval()
- setTimeout()
- structuredClone()
- window.requestAnimationFrame (dedicated workers only)
- window.cancelAnimationFrame (dedicated workers only)
以下函数仅对worker可用:
- WorkerGlobalScope.importScripts() (all workers)
- DedicatedWorkerGlobalScope.postMessage (dedicated workers only)
worker中可用的Web APIs
注意:如果列出的API在特定版本中被平台支持,那么通常可以认为它在web worker中可用。您还可以使用网站https://worker-playground.glitch.me/测试对特定对象/函数的支持
1.3 结构化克隆算法
结构化克隆算法复制复杂的JavaScript对象。它在调用structuredClone()时内部使用,通过postMessage()
在 worker 之间传输数据,使用IndexedDB存储对象,或为其他api复制对象。
它通过递归遍历输入对象进行克隆,同时维护以前访问过的引用的映射,以避免无限遍历循环。
结构化克隆不适用的东西
- 函数对象不能被结构化克隆算法复制;试图抛出
DataCloneError
异常。 - 克隆 DOM 节点同样会抛出
DataCloneError
异常。 - 某些对象属性不保留:
- 不保留RegExp对象的
lastIndex
属性。 - 属性描述符、setter、getter和类似元数据的特性不复制。例如,如果一个对象用属性描述符标记为只读,那么它将在副本中被读/写,因为这是默认的。
- 原型链(
prototype chain
)不会被遍历或复制。
- 不保留RegExp对象的
支持的类型
JavaScript types
Error types
Web/API types
1.4 Transferable objects
可转移对象(Transferable objects
)是拥有资源的对象,这些资源可以从一个上下文中转移到另一个上下文中,从而确保资源一次只在一个上下文中可用。转移后,原对象不再可用;它不再指向传输的资源,并且任何读取或写入该对象的尝试都会抛出异常。
可转移对象通常用于共享一次只能安全地暴露给单个JavaScript线程的资源。例如,ArrayBuffer是一个可转移的对象,它拥有一块内存。当这样的缓冲区在线程之间传输时,相关的内存资源将从原始缓冲区中分离出来,并附加到新线程中创建的缓冲区对象上。原始线程中的缓冲区对象不再可用,因为它不再拥有内存资源。
在使用structuredClone()
创建对象的深度拷贝时也可能使用Transferring 。克隆操作之后,转移的资源将被移动,而不是复制到克隆对象中。
用于传输对象资源的机制取决于对象本身。例如,当在线程之间传输ArrayBuffer
时,它所指向的内存资源实际上是在上下文之间以快速有效的零复制操作移动的。其他对象可以通过复制相关资源然后从旧上下文中删除来传输。
并不是所有的对象都是可转移的。下面提供了可转移对象的列表。
在线程之间传输对象
下面的代码演示了将消息从主线程发送到web worker线程时传输是如何工作的。Uint8Array在工作中被复制(复制),而它的缓冲区被转移。传输后,从主线程读取或写入uInt8Array的任何尝试都会抛出,但您仍然可以检查byteLength以确认它现在为零。
// 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
注意:类型化数组,如
Int32Array
和Uint8Array
,是可序列化的,但不可转移。然而,它们的底层缓冲区是ArrayBuffer
,这是一个可转移的对象。我们可以在data参数发送uInt8Array.buffer
。而不是传输数组中的uInt8Array
。
克隆操作期间的转移
下面的代码显示了structuredClone()
操作,其中底层缓冲区从原始对象复制到克隆对象。
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
// Transferring the Uint8Array would throw an exception as it is not a transferable object
// const transferred = structuredClone(original, {transfer: [original]});
// 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
Supported objects
注意:可转移对象在Web IDL文件中被标记为属性
[Transferable]
。
2、接口
2.1 DedicatedWorkerGlobalScope
可以通过self关键字访问DedicatedWorkerGlobalScope
对象(Worker
全局作用域)。JavaScript Reference中列出了一些额外的全局函数、命名空间对象和构造函数,它们通常不与worker全局作用域关联,但在它上可用。参见:Functions available to workers.。
2.2 ServiceWorker
2.3 SharedWorker
2.4 SharedWorkerGlobalScope
2.5 Worker
Web Workers API 的Worker
接口代表了一个可以通过脚本创建的后台任务,它可以将消息发送回它的创建者。
通过调用Worker("path/to/worker/script")
构造函数来创建 worker。
Workers 可以自己产生新的Workers ,只要这些Workers 被托管在与父页相同的origin。
并不是所有的接口和函数都对 Worker中 的脚本可用。Workers 可以使用XMLHttpRequest
进行网络通信,但是它的responseXML
和channel
属性总是为null
。(fetch
也是可用的,没有这样的限制。)
构造函数
Worker()
创建一个专用(dedicated)的web worker,执行指定的URL处的脚本。这也适用于 Blob URLs。该脚本必须遵守同源策略。
注意:浏览器制造商对于数据URL是否同源存在分歧。虽然Firefox 10及以后的版本接受数据url,但并非所有其他浏览器都是如此。
new Worker(aURL)
new Worker(aURL, options)
-
aURL
一个字符串,表示工作线程将要执行的脚本的URL。它必须遵守同源策略。 -
options 可选
一个对象,其中包含可在创建对象实例时设置的选项属性。可用的属性如下:-
type
指定要创建的工作线程类型的字符串。取值为classic
或module
。如果未指定,则使用的默认值为classic
。 -
credentials
指定工作线程使用的凭据类型的字符串。该值可以是omit
、same-origin
或include
。如果没有指定,或者如果type是classic,则默认使用omit
(不需要凭据)。 -
name
一个字符串,指定用于表示工作器范围的DedicatedWorkerGlobalScope的标识名,主要用于调试目的。
-
实例方法
- Worker.postMessage()
发送一个消息——由任何JavaScript对象组成——到worker的内部作用域。
Worker
postMessage()
方法委托给MessagePort postMessage()方法,该方法在事件循环中添加一个与接收MessagePort
相对应的任务。
Worker可以使用DedicatedWorkerGlobalScope.postMessage方法将信息发送回生成它的线程。
postMessage(message)
postMessage(message, options)
postMessage(message, transfer)
-
message
交付给worker的对象;这将在交付给DedicatedWorkerGlobalScope.message_event 事件的data
字段中。这可以是由结构化克隆算法处理的任何值或JavaScript对象,其中包括循环引用。
如果没有提供消息参数,解析器将抛出SyntaxError
。如果要传递给worker的数据不重要,则可以显式传递null
或undefined
。 -
options 可选
一个可选对象,它包含一个transfer
字段,该字段包含一组要传输其所有权的可传输对象。如果对象的所有权被转移,它将在发送它的上下文中变得不可用,并且只对发送它的 worker 可用。 -
transfer 可选
可转移对象的可选数组,用于转移其所有权。如果对象的所有权被转移,它将在发送它的上下文中变得不可用,并且只对发送它的worker 可用。
可转移对象是类的实例,如ArrayBuffer, MessagePort或ImageBitmap对象可以被转移。null
不是可接受的transfer
值。
注意:
postMessage()
一次只能发送一个对象。如上所示,如果你想传递多个值,你可以发送一个数组。
- Worker.terminate()
立即终止 worker。这不会让worker完成它的操作;它立刻停止了。ServiceWorker实例不支持此方法。
事件
-
error
在工作线程中发生错误时触发。 -
message
当worker的父进程收到来自该worker的消息时触发。 -
messageerror
当Worker对象接收到无法反序列化的消息时触发。 -
rejectionhandled
每次Promise被拒绝时触发,不管是否有处理程序来捕获拒绝。 -
unhandledrejection
当Promise拒绝而没有处理程序捕获拒绝时触发。
Example
下面的代码片段使用Worker()
构造函数创建一个Worker
对象,然后使用该Worker
对象:
const myWorker = new Worker("/worker.js");
const first = document.querySelector("input#number1");
const second = document.querySelector("input#number2");
first.onchange = () => {
myWorker.postMessage([first.value, second.value]);
console.log("Message posted to worker");
};
有关完整示例,请参阅我们的 Basic dedicated worker example
2.6 WorkerGlobalScope
Web Workers API的WorkerGlobalScope
接口是一个表示任何worker的作用域的接口。Workers 没有浏览器上下文;这个范围包含通常由Window对象传递的信息——在本例中是事件处理程序、控制台或相关的WorkerNavigator对象。每个WorkerGlobalScope
都有自己的事件循环。
这个接口通常被每个worker类型特例化:dedicated worker的专用 DedicatedWorkerGlobalScope , shared worker的共享SharedWorkerGlobalScope, ServiceWorker的ServiceWorkerGlobalScope。self
属性返回每个上下文的专用范围。
Example
你不能在代码中直接访问WorkerGlobalScope
;然而,它的属性和方法是由更具体的全局作用域继承的,比如DedicatedWorkerGlobalScope
和SharedWorkerGlobalScope
。例如,你可以将另一个脚本导入到worker中,并使用以下两行打印出worker作用域的导航器对象的内容:
importScripts("foo.js");
console.log(navigator);
2.7 WorkerLocation
WorkerLocation
接口定义了Worker
执行的脚本的绝对位置。这样的对象为每个worker初始化,并通过WorkerGlobalScope可用。通过调用self.location获得的Location属性。
2.8 WorkerNavigator
WorkerNavigator
接口表示允许从 Worker 访问的Navigator接口的一个子集。这样的对象为每个Worker
初始化,并通过self.navigator属性可用。