Appearance
使用 Web Worker
Web Worker 为 Web 内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,它们可以使用 XMLHttpRequest(尽管 responseXML 和 channel 属性总是为空)或 fetch(没有这些限制)执行 I/O。一旦创建,一个 worker 可以将消息发送到创建它的 JavaScript 代码,通过将消息发布到该代码指定的事件处理器(反之亦然)
Web Worker API
一个 worker 是使用一个构造函数创建的一个对象(例如 Worker())运行一个命名的 JavaScript 文件——这个文件包含将在 worker 线程中运行的代码; worker 运行在另一个全局上下文中,不同于当前的window。因此,在 Worker 内通过 window 获取全局作用域(而不是self)将返回错误。
在专用 worker 的情况下,DedicatedWorkerGlobalScope 对象代表了 worker 的上下文(专用 worker 是指标准 worker 仅在单一脚本中被使用;共享 worker 的上下文是 SharedWorkerGlobalScope 对象)。一个专用 worker 仅能被首次生成它的脚本使用,而共享 worker 可以同时被多个脚本使用。
在 worker 线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在 worker 内,不能直接操作 DOM 节点,也不能使用 window 对象的默认方法和属性。但是你可以使用大量 window 对象之下的东西,包括 WebSockets,以及 IndexedDB 等数据存储机制。查看 Web Worker 可以使用的函数和类获取详情。
workers 和主线程间的数据传递通过这样的消息机制进行——双方都使用 postMessage() 方法发送各自的消息,使用 onmessage 事件处理函数来响应消息(消息被包含在 message 事件的 data 属性中)。这个过程中数据并不是被共享而是被复制。
只要运行在同源的父页面中,worker 可以依次生成新的 worker;并且可以使用 XMLHttpRequest 进行网络 I/O,但是 XMLHttpRequest 的 responseXML 和 channel 属性总会返回 null。
专用 Worker
如前文所述,一个专用 worker 仅能被生成它的脚本所使用。
worker 特性检测
js
if (window.Worker) {
// 支持
}创建 worker
调用 Worker() 构造器,指定一个脚本的 URI 来执行 worker 线程(main.js):
js
const worker = new Worker('worker.js')传递信息和接收消息
通过 postMessage() 方法和 onmessage 事件处理函数触发 worker 的方法。当你想要向一个 worker 发送消息时,你只需要这样做:
js
worker.postMessage('Hello World')worker 中接收到消息后,我们可以写这样一个事件处理函数代码作为响应(worker.js):
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 属性进行使用。这里我们简单的对这 2 个数字作乘法处理并再次使用 postMessage() 方法,将结果回传给主线程。
回到主线程,我们再次使用 onmessage 以响应 worker 回传的消息:
js
worker.onmessage = (e) => {
console.log('Message received from worker')
console.log(e.data)
}在主线程中使用时,
onmessage和postMessage()必须挂在worker对象上,而在worker中使用时不用这样做。原因是,在worker内部,worker是有效的全局作用域。
当一个消息在主线程和worker之间传递时,它被复制或者转移了,而不是共享。
终止 worker
当 worker 不再需要时,最好将其终止。你可以通过 terminate() 方法完成这个操作:
js
worker.terminate()处理错误
worker 内部抛出的任何异常都会被 worker 内部的 onerror 事件处理函数捕获。你可以通过 worker.onerror 属性来访问这个事件处理函数。
该事件不会冒泡并且可以被取消;为了防止触发默认动作,worker 可以调用错误事件的 preventDefault() 方法。
错误事件有以下三个用户关心的字段:
message
可读性良好的错误消息。filename
发生错误的脚本文件名。lineno
发生错误时所在脚本文件的行号。
生成 subworker
如果需要的话,worker 能够生成更多的 worker,这就是所谓的 subworker,它们必须托管在同源的父页面内。而且,subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。这使得 worker 更容易记录它们之间的依赖关系。
引入脚本与库
Worker 线程能够访问一个全局函数 importScripts() 来引入脚本,该函数接受 0 个或者多个 URI 作为参数来引入资源;以下例子都是合法的:
js
importScripts() /* 什么都不引入 */
importScripts('script1.js')
importScripts('script2.js', 'script3.js')
importScripts('https://example.com/script4.js')浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。如果脚本无法加载,将抛出 NETWORK_ERROR 异常,接下来的代码也无法执行。而之前执行的代码(包括使用 setTimeout() 异步执行的代码)依然能够运行。importScripts() 之后的函数声明依然会被保留,因为它们始终会在其他代码之前运行。
共享 Worker
一个共享 worker 可以被多个脚本使用——即使这些脚本正在被不同的 window、iframe 或者 worker 访问。
如果共享 worker 可以被多个浏览上下文调用,所有这些浏览上下文必须属于同源(相同的协议,主机和端口号)。
生成一个共享 Worker
要创建一个共享 worker,你只需要在 Worker() 构造函数中指定一个脚本 URI,并在前面加上 shared 关键字:
js
const worker = new SharedWorker('worker.js')与一个共享 worker 通信必须通过 port 对象——一个确切的打开的端口供脚本与 worker 通信(在专用 worker 中这一部分是隐式进行的)。
在传递消息之前,端口连接必须被显式的打开,打开方式是使用 onmessage 事件处理函数或者 start() 方法。只有一种情况下需要调用 start() 方法,那就是 message 事件被 addEventListener() 方法使用。
port 对象包含以下属性:
port.start()port.postMessage()port.onmessageport.close()port.start()port.onmessageerror
在使用
start()方法打开端口连接时,如果父级线程和worker线程需要双向通信,那么它们都需要调用该方法。
共享 Worker 的消息传递
共享 worker 的消息传递机制与专用 worker 相同,只是消息是通过 port 对象传递的,而不是 worker 对象。
js
squareNumber.onchange = () => {
myWorker.port.postMessage([squareNumber.value, squareNumber.value])
console.log('Message posted to worker')
}回到 Worker 中,这里有些不一样
js
onconnect = (e) => {
const port = e.ports[0]
port.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')
port.postMessage(workerResult)
}
}首先,当一个端口连接被创建时(例如:在父级线程中,设置 onmessage 事件处理函数,或者显式调用 start() 方法时),使用 onconnect 事件处理函数来执行代码。
使用事件的 ports 属性来获取端口并存储在变量中。
然后,为端口添加一个 onmessage 处理函数用来做运算并回传结果给主线程。在 worker 线程中设置此 onmessage 处理函数也会隐式的打开与主线程的端口连接,因此这里跟前文一样,对 port.start() 的调用也是不必要的。
然后回到主线程,我们再次使用 port.onmessage 以响应 worker 回传的消息:
js
myWorker.port.onmessage = (e) => {
result2.textContent = e.data
console.log('Message received from worker')
}关于线程安全
Worker 接口会生成真正的操作系统级别的线程,如果你不太小心,那么并发会对你的代码产生有趣的影响。
然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。
内容安全策略
有别于创建它的 document 对象,worker 有它自己的执行上下文。因此普遍来说,worker 并不受限于创建它的 document(或者父级 worker)的内容安全策略。我们来举个例子,假设一个 document 有如下头部声明:
html
<head>
<meta http-equiv="Content-Security-Policy" content="script-src 'self'" />
</head>CSP (Content Security Policy) 是一种重要的安全机制,用于防止 XSS 攻击。当设置为 script-src 'self' 时,它限制只能执行与页面同源的脚本。
这个声明有一部分作用在于,禁止它内部包含的脚本代码使用 eval() 方法。然而,如果脚本代码创建了一个 worker,在 worker 上下文中执行的代码却是可以使用 eval() 的。
为了给 worker 指定内容安全策略,必须为发送 worker 代码的请求本身设置 Content-Security-Policy 响应标头
有一个例外情况,即 worker 脚本的源如果是一个全局性的唯一的标识符(例如,它的 URL 协议为 data 或 blob),worker 则会继承创建它的 document 或者 worker 的 CSP。
Worker 中的数据接收与发送:详细介绍
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。大部分浏览器使用结构化克隆来实现该特性。
让我们创建一个名为 emulateMessage() 的函数,它将模拟在从 worker 到主页面(反之亦然)的通信过程中,变量的“拷贝而非共享”行为:
js
function emulateMessage(vVal) {
return eval(`(${JSON.stringify(vVal)})`)
}
// Tests
// test #1
const example1 = new Number(3)
console.log(typeof example1) // object
console.log(typeof emulateMessage(example1)) // number
// test #2
const example2 = true
console.log(typeof example2) // boolean
console.log(typeof emulateMessage(example2)) // boolean
// test #3
const example3 = new String('Hello World')
console.log(typeof example3) // object
console.log(typeof emulateMessage(example3)) // string
// test #4
const example4 = {
name: 'Carina Anand',
age: 43,
}
console.log(typeof example4) // object
console.log(typeof emulateMessage(example4)) // object
// test #5
function Animal(type, age) {
this.type = type
this.age = age
}
const example5 = new Animal('Cat', 3)
console.log(example5.constructor) // Animal
console.log(emulateMessage(example5).constructor) // Object拷贝而并非共享的那个值称为消息(message)。再来谈谈 worker,你可以使用 postMessage() 将消息传递给主线程或从主线程传送回来。message 事件的 data 属性就包含了从 worker 传回来的数据
example.html
js
const myWorker = new Worker('my_task.js')
myWorker.onmessage = (event) => {
console.log(`Worker said : ${event.data}`)
}
myWorker.postMessage('ali')my_task.js(worker)
js
postMessage("I'm working before postMessage('ali').")
onmessage = (event) => {
postMessage(`Hi, ${event.data}`)
}结构化克隆算法可以接收 JSON 数据以及一些 JSON 不能表示的数据——比如循环引用。
传递数据的例子
传输 JSON 的高级方式和创建一个交换系统
如果你需要传输非常复杂的数据,还要同时在主页与 Worker 内调用多个方法,那么可以考虑创建一个类似下面的系统。
首先,我们创建一个 QueryableWorker 的类,它接收 worker 的 URL、一个默认侦听函数和一个错误处理函数作为参数,这个类将会记录所有的侦听的列表并且帮助我们与 worker 进行通信。
js
function QueryableWorker(url, defaultListener, onError) {
const instance = this
const worker = new Worker(url)
const listeners = {}
this.defaultListener = defaultListener ?? (() => {})
if (onError) {
worker.onerror = onError
}
this.postMessage = (message) => {
worker.postMessage(message)
}
this.terminate = () => {
worker.terminate()
}
}写出新增和删除侦听的方法
js
this.addListeners = (name, listener) => {
listeners[name] = listener
}
this.removeListeners = (name) => {
delete listeners[name]
}这里我们让 worker 处理 2 个这样的简单操作:区别 2 个数字并在 3 秒后弹框提示。为了完成这个操作,我们首先实现一个 sendQuery 方法,该方法可以查询 worker 是否真正有我们所需要的对应方法。
js
// 该函数至少需要一个参数,即我们想要查询的方法名称。
// 然后我们可以传入方法所需的参数。
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError('QueryableWorker.sendQuery takes at least one argument')
}
worker.postMessage({
queryMethod,
queryMethodArguments,
})
}我们以 onmessage 方法作为 QueryableWorker 的结尾。如果 worker 有我们所需要的对应的方法,它就会返回相对应的侦听方法的名字以及所需要的参数,我们只需要在侦听列表 listeners 中找到它:
js
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, 'queryMethodListener') &&
Object.hasOwn(event.data, 'queryMethodArguments')
) {
listeners[event.data.queryMethodListener].apply(instance, event.data.queryMethodArguments)
} else {
this.defaultListener.call(instance, event.data)
}
}现在回到 worker 中。首先我们需要一个能够完成这 2 个操作的方法:
js
const queryableFunctions = {
getDifference(a, b) {
reply('printStuff', a - b)
},
waitSomeTime() {
setTimeout(() => {
reply('doAlert', 3, 'seconds')
}, 3000)
},
}
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError('reply - takes at least one argument')
}
postMessage({
queryMethodListener,
queryMethodArguments,
})
}
// 当主页面直接调用 QueryWorker 的 postMessage 方法时,该方法被调用。
function defaultReply(message) {
// 做点什么
}onmessage 方法也就很简单了:
js
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, 'queryMethod') &&
Object.hasOwn(event.data, 'queryMethodArguments')
) {
queryableFunctions[event.data.queryMethod].apply(self, event.data.queryMethodArguments)
} else {
defaultReply(event.data)
}
}接下来给出一个完整的实现:
html
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>MDN Example - Queryable worker</title>
<script type="text/javascript">
// QueryableWorker 实例的方法:
// * sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc.): 调用一个 Worker 的可查询函数
// * postMessage(string or JSON Data): 见 Worker.prototype.postMessage()
// * terminate(): 终止 Worker
// * addListener(name, function): 添加一个监听器
// * removeListener(name): 移除一个监听器
// QueryableWorker 实例的属性:
// * defaultListener: 默认监听器只在 Worker 直接调用 postMessage() 函数时执行
function QueryableWorker(url, defaultListener, onError) {
const instance = this
const worker = new Worker(url)
const listeners = {}
this.defaultListener = defaultListener ?? (() => {})
if (onError) {
worker.onerror = onError
}
this.postMessage = (message) => {
worker.postMessage(message)
}
this.terminate = () => {
worker.terminate()
}
this.addListener = (name, listener) => {
listeners[name] = listener
}
this.removeListener = (name) => {
delete listeners[name]
}
// 这个函数至少需要一个参数,即我们想要查询的方法名称。
// 然后我们可以传入方法所需的参数。
this.sendQuery = (queryMethod, ...queryMethodArguments) => {
if (!queryMethod) {
throw new TypeError('QueryableWorker.sendQuery takes at least one argument')
}
worker.postMessage({
queryMethod,
queryMethodArguments,
})
}
worker.onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, 'queryMethodListener') &&
Object.hasOwn(event.data, 'queryMethodArguments')
) {
listeners[event.data.queryMethodListener].apply(
instance,
event.data.queryMethodArguments,
)
} else {
this.defaultListener.call(instance, event.data)
}
}
}
// 你自定义的 "queryable" worker
const myTask = new QueryableWorker('my_task.js')
// 你自定义的 "listeners"
myTask.addListener('printStuff', (result) => {
document
.getElementById('firstLink')
.parentNode.appendChild(document.createTextNode(`The difference is ${result}!`))
})
myTask.addListener('doAlert', (time, unit) => {
alert(`Worker waited for ${time} ${unit} :-)`)
})
</script>
</head>
<body>
<ul>
<li>
<a id="firstLink" href="javascript:myTask.sendQuery('getDifference', 5, 3);"
>What is the difference between 5 and 3?</a
>
</li>
<li>
<a href="javascript:myTask.sendQuery('waitSomeTime');">Wait 3 seconds</a>
</li>
<li>
<a href="javascript:myTask.terminate();">terminate() the Worker</a>
</li>
</ul>
</body>
</html>my_task.js(worker 文件):
js
const queryableFunctions = {
// 示例 1:得到两个数字的差值:
getDifference(minuend, subtrahend) {
reply('printStuff', minuend - subtrahend)
},
// 示例 2:等待三秒
waitSomeTime() {
setTimeout(() => {
reply('doAlert', 3, 'seconds')
}, 3000)
},
}
// 系统函数
function defaultReply(message) {
// 你的默认 PUBLIC 函数只在主页面直接调用 queryableWorker.postMessage() 方法时执行。
// 做点什么
}
function reply(queryMethodListener, ...queryMethodArguments) {
if (!queryMethodListener) {
throw new TypeError('reply - not enough arguments')
}
postMessage({
queryMethodListener,
queryMethodArguments,
})
}
onmessage = (event) => {
if (
event.data instanceof Object &&
Object.hasOwn(event.data, 'queryMethod') &&
Object.hasOwn(event.data, 'queryMethodArguments')
) {
queryableFunctions[event.data.queryMethod].apply(self, event.data.queryMethodArguments)
} else {
defaultReply(event.data)
}
}