面向 Web 开发人员版 — 最后更新时间 2024 年 9 月 12 日
Worklets 是一部分规范基础设施,可用于运行独立于主 JavaScript 执行环境的脚本,同时不需要任何特定实现模型。
此处指定的 worklet 基础设施不能被 Web 开发人员直接使用。相反,其他规范在此基础上构建,以创建直接可用的 worklet 类型,专门用于在浏览器实现管道特定部分中运行。
允许扩展渲染或实现管道其他敏感部分(如音频输出)的扩展点非常困难。如果扩展点以对 Window
上可用 API 的完全访问权限来完成,引擎将需要放弃之前对那些阶段中可能发生的事情的假设。例如,在布局阶段,渲染引擎假设不会修改任何 DOM。
此外,在 Window
环境中定义扩展点会将用户代理限制在与 Window
对象相同的线程中执行工作。(除非实现添加了复杂、高开销的基础设施来允许线程安全 API,以及线程连接保证。)
Worklets 旨在允许扩展点,同时保持用户代理当前依赖的保证。这是通过基于 WorkletGlobalScope
子类的新的全局环境来完成的。
Worklets 类似于 Web 工作线程。但是,它们
与线程无关。也就是说,它们不是设计在专用独立线程上运行的,就像每个工作线程一样。实现可以在它们选择的地方运行 worklets(包括在主线程上)。
能够创建多个全局作用域的重复实例,以实现并行性。
不使用基于事件的 API。相反,类是在全局作用域上注册的,其方法由用户代理调用。
在全局作用域上具有减少的 API 表面。
其 全局对象 的生命周期由其他规范定义,通常以 实现定义 的方式。
由于 worklets 的开销相对较高,因此最好谨慎使用。因此,预期给定 WorkletGlobalScope
将在多个独立脚本之间共享。(这类似于单个 Window
如何在多个独立脚本之间共享。)
Worklets 是一项通用技术,可用于不同的用例。一些 worklets(例如在 CSS 绘画 API 中定义的 worklets)提供旨在用于无状态、幂等和短运行计算的扩展点,这些扩展点具有下一节中描述的特殊考虑因素。其他 worklets(例如在 Web 音频 API 中定义的 worklets)用于有状态、长时间运行的操作。 [CSSPAINT] [WEBAUDIO]
一些使用 worklets 的规范旨在允许用户代理在多个线程上并行执行工作,或根据需要在线程之间移动工作。在这些规范中,用户代理可能会以 实现定义 的顺序调用 Web 开发人员提供的类上的方法。
因此,为了防止互操作性问题,在这些 WorkletGlobalScope
上注册类的作者应该使他们的代码幂等。也就是说,类上的方法或方法集对于给定特定输入应该产生相同的输出。
本规范使用以下技术来鼓励作者以幂等的方式编写代码
全局对象不可用(即,在 WorkletGlobalScope
上没有对应于 self
的内容)。
尽管这是 worklets 首次指定时的意图,但 globalThis
的引入使其不再属实。有关更多讨论,请参阅 问题 #6059。
代码以 模块脚本 的形式加载,这会导致代码在严格模式下执行,并且没有共享的 this
引用全局代理。
这些限制共同有助于防止两个不同的脚本使用 全局对象 的属性共享状态。
此外,使用 worklets 并打算允许 实现定义 行为的规范必须遵守以下内容
它们必须要求用户代理始终为每个 Worklet
至少拥有两个 WorkletGlobalScope
实例,并随机将类上的方法或方法集分配给特定 WorkletGlobalScope
实例。这些规范可能会在内存限制下提供选择退出。
这些规范必须允许用户代理随时创建和销毁其 WorkletGlobalScope
子类的实例。
一些使用 worklets 的规范可以根据用户代理的状态调用 Web 开发人员提供的类上的方法。为了提高线程之间的并发性,用户代理可能会基于潜在的未来状态推测性地调用方法。
在这些规范中,用户代理可能会在任何时候调用这些方法,并使用任何参数,而不仅仅是与用户代理的当前状态相对应的参数。这种推测性评估的结果不会立即显示,但可以缓存以供用户代理状态与推测状态匹配时使用。这可以提高用户代理和 worklet 线程之间的并发性。
因此,为了防止用户代理之间发生互操作性风险,在这些 WorkletGlobalScope
上注册类的作者应该使他们的代码无状态。也就是说,调用方法的唯一影响应该是其结果,而不是任何副作用,例如更新可变状态。
鼓励 代码幂等性 的相同技术也鼓励作者编写无状态代码。
对于这些示例,我们将使用一个假 worklet。Window
对象提供两个 Worklet
实例,每个实例都在其自己的 FakeWorkletGlobalScope
集合中运行代码
window.fakeWorklet1
window.fakeWorklet2
这两者的 worklet 全局作用域类型 都设置为 FakeWorkletGlobalScope
,其 worklet 目标类型 都设置为 "fakeworklet
"。用户代理应该为每个 worklet 创建至少两个 FakeWorkletGlobalScope
实例。
"fakeworklet
" 实际上不是根据 Fetch 的有效 目标。但这说明了真正的 worklets 通常如何具有它们自己的 worklet 类型特定的目标。 [FETCH]
在 FakeWorkletGlobalScope
内部,以下全局方法可用
registerFake(type, classConstructor)
要将脚本加载到 假 worklet 1 中,Web 开发人员会编写
window. fakeWorklet1. addModule( 'script1.mjs' );
window. fakeWorklet1. addModule( 'script2.mjs' );
请注意,哪个脚本先完成获取并运行取决于网络计时:可能是 script1.mjs
或 script2.mjs
。这对于为加载到 worklets 中而编写的、编写良好的脚本来说通常并不重要,如果它们遵循有关准备进行 推测性评估 的建议。
如果 Web 开发人员想要仅在脚本成功运行并加载到某些 worklets 中后执行任务,他们可以编写
Promise. all([
window. fakeWorklet1. addModule( 'script1.mjs' ),
window. fakeWorklet2. addModule( 'script2.mjs' )
]). then(() => {
// Do something which relies on those scripts being loaded.
});
关于脚本加载的另一个重要要点是,加载的脚本可以在每个 Worklet
中的多个 WorkletGlobalScope
中运行,如关于 代码幂等性 的部分所述。特别是,上述关于 假 worklet 1 和 假 worklet 2 的规范要求这样做。因此,请考虑以下场景
// script.mjs
console. log( "Hello from a FakeWorkletGlobalScope!" );
// app.mjs
window. fakeWorklet1. addModule( "script.mjs" );
这可能导致用户代理控制台输出如下内容
[fakeWorklet1#1] Hello from a FakeWorkletGlobalScope!
[fakeWorklet1#4] Hello from a FakeWorkletGlobalScope!
[fakeWorklet1#2] Hello from a FakeWorkletGlobalScope!
[fakeWorklet1#3] Hello from a FakeWorkletGlobalScope!
如果用户代理在某个时候决定终止和重启 FakeWorkletGlobalScope
的第三个实例,控制台将在发生这种情况时再次打印 [fakeWorklet1#3] Hello from a FakeWorkletGlobalScope!
。
假设 Web 开发人员使用我们假 worklet 的意图之一是允许他们自定义高度复杂的布尔否定过程。他们可能会注册他们的自定义内容,如下所示
// script.mjs
registerFake( 'negation-processor' , class {
process( arg) {
return ! arg;
}
});
// app.mjs
window. fakeWorklet1. addModule( "script.mjs" );
为了利用这些注册的类,假 worklet 的规范可以定义一个 查找真值的相反值 算法,给定一个 Worklet
worklet,它会调用作为类型 "negation-processor
" 注册到 worklet 的全局作用域之一上的任何类的 process
方法,并使用 true 作为参数,然后以某种方式使用结果。
WorkletGlobalScope
的子类用于创建 全局对象,其中加载到特定 Worklet
中的代码可以执行。
其他规范旨在子类化 WorkletGlobalScope
,添加注册类的 API 以及特定于其工作线程类型的其他 API。
每个 WorkletGlobalScope
都包含在它自己的 工作线程代理 中,该代理具有其对应的 事件循环。但是,在实践中,这些代理和事件循环的实现预计将与大多数其他代理和事件循环不同。
每个 WorkletGlobalScope
都存在一个 工作线程代理,因为理论上,实现可以使用一个独立的线程来处理每个 WorkletGlobalScope
实例,并且允许这种级别的并行性最好使用代理来完成。但是,由于它们的 [[CanBlock]] 值为 false,因此代理和线程之间没有一对一的必要性。这允许实现自由地在任何线程上执行加载到工作线程中的脚本,包括运行来自其他具有 [[CanBlock]] 为 false 的代理的代码的线程,例如 相同来源窗口代理(“主线程”)的线程。将此与 专用工作线程代理 相比,它们对 [[CanBlock]] 的真实值实际上要求它们获得一个专用的操作系统线程。
工作线程的 事件循环 也是有点特殊的。它们仅用于与 addModule()
相关的任务,即用户代理调用作者定义的方法的任务,以及微任务。因此,即使 事件循环处理模型 指定所有事件循环都持续运行,实现也可以使用更简单的策略来实现可观察的等效结果,该策略只是 调用 作者提供的函数,然后依赖于该过程来 执行微任务检查点。
Worklet
类在所有当前引擎中支持。
Worklet
类提供了将模块脚本添加到其关联的 WorkletGlobalScope
的功能。然后,用户代理可以创建在 WorkletGlobalScope
上注册的类并调用它们的方法。
创建 Worklet
实例的规范必须为给定实例指定以下内容
它的 工作线程全局范围类型,它必须是 Web IDL 类型,该类型 继承 自 WorkletGlobalScope
;以及
它的 工作线程目标类型,它必须是 目标,并且在获取脚本时使用。
await worklet.addModule(moduleURL[, { credentials }])
将 moduleURL 给出的 模块脚本 加载并执行到所有 worklet 的 全局范围 中。它还可以在此过程中创建额外的全局范围,具体取决于工作线程类型。返回的 Promise 在脚本已成功加载并运行在所有全局范围中后将完成。
credentials
选项可以设置为 凭据模式 来修改脚本获取过程。它默认为“same-origin
”。
任何 获取 脚本或其依赖项的失败都会导致返回的 Promise 被拒绝,并使用一个 "AbortError
" DOMException
。任何解析脚本或其依赖项的错误都会导致返回的 Promise 被拒绝,并使用解析期间生成的异常。
Worklet
的生命周期没有特殊注意事项;它与它所属的对象绑定,例如 Window
。
WorkletGlobalScope
的生命周期至少与包含它的 工作线程全局范围 的 Document
绑定。特别是,销毁 Document
将 终止 对应的 WorkletGlobalScope
并允许它被垃圾回收。
此外,用户代理可以在任何时候 终止 给定的 WorkletGlobalScope
,除非定义相应工作线程类型的规范另有规定。例如,如果 工作线程代理 的 事件循环 没有排队的 任务,或者如果用户代理没有计划使用工作线程的挂起操作,或者如果用户代理检测到异常操作(例如无限循环或超过设定的时间限制的回调),它们可能会终止它们。
最后,特定工作线程类型的规范可以提供有关何时 创建 给定工作线程类型的 WorkletGlobalScope
的更多具体细节。例如,它们可能会在调用工作线程代码的特定过程中创建它们,就像在 示例 中一样。