
2025-04-23 日报 Day165

今日的鸡汤
有风有雨是常态,风雨无阻是心态,风雨兼程是状态。
今日学习内容
1、JS 红皮书 P796-800 第二十七章:工作者线程
今日笔记
1、专用工作者线程与隐式 MessagePorts: 专用工作者线程的 Worker 对象和 DedicatedWorkerGlobalScope 与 MessagePorts 有一些相同接口处理程序和方法:onmessage、onmessageerror、close()和 postMessage()。这不是偶然的,因为专用工作者线程隐式使用了 MessagePorts 在两个上下文之间通信。
父上下文中的 Worker 对象和 DedicatedWorkerGlobalScope 实际上融合了 MessagePort,并在自己的接口中分别暴露了相应的处理程序和方法。换句话说,消息还是通过 MessagePort 发送,只是没有直接使用 MessagePort 而已。
也有不一致的地方,比如 start()和 close()约定。专用工作者线程会自动发送排队的消息,因此 start()也就没有必要了。另外,close()在专用工作者线程的上下文中没有意义,因为这样关闭MessagePort 会使工作者线程孤立。因此,在工作者线程内部调用 close()(或在外部调用terminate())不仅会关闭 MessagePort,也会终止线程。
2、专用工作者线程的生命周期: 调用 Worker()构造函数是一个专用工作者线程生命的起点。调用之后,它会初始化对工作者线程脚本的请求,并把 Worker 对象返回给父上下文。虽然父上下文中可以立即使用这个 Worker 对象,但与之关联的工作者线程可能还没有创建,因为存在请求脚本的网格延迟和初始化延迟。
一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始化(initializing)、活动(active)和终止(terminated)。这几个状态对其他上下文是不可见的。虽然 Worker 对象可能会存在于父上下文中,但也无法通过它确定工作者线程当前是处理初始化、活动还是终止状态。换句话说,与活动的专用工作者线程关联的 Worker 对象和与终止的专用工作者线程关联的 Worker 对象无法分别。
初始化时,虽然工作者线程脚本尚未执行,但可以先把要发送给工作者线程的消息加入队列。这些消息会等待工作者线程的状态变为活动,再把消息添加到它的消息队列。下面的代码演示了这个过程。
initializingWorker.js
self.addEventListener(‘message’, ({data}) => console.log(data));
main.js
const worker = new Worker(‘./initializingWorker.js’);
// Worker 可能仍处于初始化状态
// 但 postMessage()数据可以正常处理
worker.postMessage(‘foo’);
worker.postMessage(‘bar’);
worker.postMessage(‘baz’);
// foo
// bar
// baz
创建之后,专用工作者线程就会伴随页面的整个生命期而存在,除非自我终止(self.close())或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。只要工作者线程仍存在,与之关联的 Worker 对象就不会被当成垃圾收集掉。
自我终止和外部终止最终都会执行相同的工作者线程终止例程。来看下面的例子,其中工作者线程在发送两条消息中间执行了自我终止:
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
虽然调用了 close(),但显然工作者线程的执行并没有立即终止。close()在这里会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务。这也是为什么”baz”没有打印出来的原因。工作者线程不需要执行同步停止,因此在父上下文的事件循环中处理的”bar”仍会打印出来。
下面来看外部终止的例子。
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
这里,外部先给工作者线程发送了带”foo”的 postMessage,这条消息可以在外部终止之前处理。一旦调用了 terminate(),工作者线程的消息队列就会被清理并锁住,这也是只是打印”foo”的原因。
注意 close()和 terminate()是幂等操作,多次调用没有问题。这两个方法仅仅是将Worker 标记为 teardown,因此多次调用不会有不好的影响。
在整个生命周期中,一个专用工作者线程只会关联一个网页(Web 工作者线程规范称其为一个文档)。除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或关闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。
3、配置Worker选项: Worker()构造函数允许将可选的配置对象作为第二个参数。该配置对象支持下列属性。
name:可以在工作者线程中通过 self.name 读取到的字符串标识符。
type:表示加载脚本的运行方式,可以是”classic”或”module”。”classic”将脚本作为常规脚本来执行,”module”将脚本作为模块来执行。
credentials:在 type 为”module”时,指定如何获取与传输凭证数据相关的工作者线程模块脚本。值可以是”omit”、”same-orign”或”include”。这些选项与 fetch()的凭证选项相同。在 type 为”classic”时,默认为”omit”。
4、在javaScript行内创建工作者线程: 工作者线程需要基于脚本文件来创建,但这并不意味着该脚本必须是远程资源。专用工作者线程也可以通过 Blob 对象 URL 在行内脚本创建。这样可以更快速地初始化工作者线程,因为没有网络延迟。
1 | // 创建要执行的 JavaScript 代码字符串 |
在这个例子中,通过脚本字符串创建了 Blob,然后又通过 Blob 创建了对象 URL,最后把对象 URL传给了 Worker()构造函数。该构造函数同样创建了专用工作者线程。
这里有意使用了斐波那契数列的实现,将其序列化之后传给了工作者线程。该函数作为 IIFE 调用并传递参数,结果则被发送回主线程。虽然计算斐波那契数列比较耗时,但所有计算都会委托到工作者线程,因此并不会影响父上下文的性能。
注意 像这样序列化函数有个前提,就是函数体内不能使用通过闭包获得的引用,也包括全局变量,比如 window,因为这些引用在工作者线程中执行时会出错。
5、在工作者线程中动态执行脚本: 工作者线程中的脚本并非铁板一块,而是可以使用 importScripts()方法通过编程方式加载和执行任意脚本。该方法可用于全局 Worker 对象。这个方法会加载脚本并按照加载顺序同步执行。比如,下面的例子加载并执行了两个脚本:
main.js
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’);
importScripts(‘./scriptB.js’);
console.log(‘scripts imported’);
importScripts()方法可以接收任意数量的脚本作为参数。浏览器下载它们的顺序没有限制,但执行则会严格按照它们在参数列表的顺序进行。因此,下面的代码与前面的效果一样:
console.log(‘importing scripts’);
importScripts(‘./scriptA.js’, ‘./scriptB.js’);
console.log(‘scripts imported’);
脚本加载受到常规 CORS 的限制,但在工作者线程内部可以请求来自任何源的脚本。这里的脚本导入策略类似于使用生成的