活文档 — 最后更新于 2024年9月12日
本节是非规范性的。
Worklets 是一部分规范基础设施,可用于运行独立于主 JavaScript 执行环境的脚本,同时不需要任何特定的实现模型。
此处指定的 worklet 基础设施无法被 Web 开发人员直接使用。相反,其他规范基于它创建直接可用的 worklet 类型,专门用于在浏览器实现管道的特定部分运行。
本节是非规范性的。
允许扩展渲染或实现管道其他敏感部分(如音频输出)的扩展点非常困难。如果扩展点使用对 Window
上提供的 API 的完全访问权限来完成,引擎将需要放弃之前对这些阶段中间可能发生的事情所做的假设。例如,在布局阶段,渲染引擎假设不会修改 DOM。
此外,在 Window
环境中定义扩展点会限制用户代理在与 Window
对象相同的线程中执行工作。(除非实现添加了复杂的高开销基础设施以允许线程安全 API 以及线程连接保证。)
Worklets 旨在允许扩展点,同时保持用户代理当前依赖的保证。这是通过基于 WorkletGlobalScope
的子类的新的全局环境来完成的。
Worklets 类似于 Web Workers。但是,它们
与线程无关。也就是说,它们并非设计为在专用的单独线程上运行,就像每个 worker 一样。实现可以在它们选择的任何位置运行 worklets(包括在主线程上)。
能够为并行处理创建多个全局作用域的重复实例。
不使用基于事件的 API。相反,类在全局作用域上注册,其方法由用户代理调用。
在全局作用域上具有减少的 API 表面。
由于 worklets 开销相对较高,因此最好谨慎使用它们。因此,预计给定的 WorkletGlobalScope
将在多个单独的脚本之间共享。(这类似于单个 Window
如何在多个单独的脚本之间共享。)
Worklets 是一种通用技术,可用于不同的用例。一些 worklets,例如在 CSS 绘制 API 中定义的那些,提供了旨在用于无状态、幂等和短运行计算的扩展点,这些扩展点具有下一节中描述的特殊注意事项。其他一些,例如在 Web 音频 API 中定义的那些,用于有状态、长时间运行的操作。 [CSSPAINT] [WEBAUDIO]
一些使用 worklets 的规范旨在允许用户代理在多个线程上并行化工作,或根据需要在线程之间移动工作。在这些规范中,用户代理可能会以 实现定义 的顺序调用 Web 开发人员提供的类上的方法。
因此,为了防止互操作性问题,在这样的 WorkletGlobalScope
上注册类的作者应该使他们的代码幂等。也就是说,给定特定输入,类上的方法或一组方法应该产生相同的输出。
此规范使用以下技术来鼓励作者以幂等的方式编写代码
无法引用全局对象(即,在 WorkletGlobalScope
上没有对应于 self
的内容)。
虽然这是在首次指定 worklets 时最初的意图,但 globalThis
的引入使其不再属实。有关更多讨论,请参阅 问题 #6059。
代码作为 模块脚本 加载,这会导致代码在严格模式下执行并且没有共享的 this
引用全局代理。
总之,这些限制有助于防止两个不同的脚本使用 全局对象 的属性共享状态。
此外,使用 worklets 并打算允许 实现定义 行为的规范必须遵守以下规定
它们必须要求用户代理每个 Worklet
至少具有两个 WorkletGlobalScope
实例,并将类上的方法或一组方法随机分配给特定的 WorkletGlobalScope
实例。这些规范可能会在内存限制下提供一种选择退出机制。
这些规范必须允许用户代理随时创建和销毁其 WorkletGlobalScope
子类的实例。
一些使用 worklets 的规范可以根据用户代理的状态调用 Web 开发人员提供的类上的方法。为了提高线程之间的并发性,用户代理可能会根据潜在的未来状态推测性地调用方法。
在这些规范中,用户代理可能会随时调用此类方法,并使用任何参数,而不仅仅是对应于用户代理当前状态的参数。此类推测性评估的结果不会立即显示,但可以缓存以供使用,如果用户代理状态与推测的状态匹配。这可以提高用户代理和 worklet 线程之间的并发性。
因此,为了防止用户代理之间的互操作性风险,在这样的 WorkletGlobalScope
上注册类的作者应该使他们的代码无状态。也就是说,调用方法的唯一效果应该是其结果,而不是任何副作用,例如更新可变状态。
鼓励 代码幂等性 的相同技术也鼓励作者编写无状态代码。
本节是非规范性的。
对于这些示例,我们将使用一个假的 worklet。 Window
对象提供两个 Worklet
实例,每个实例都在其自己的 FakeWorkletGlobalScope
集合中运行代码
partial interface Window {
[SameObject , SecureContext ] readonly attribute Worklet fakeWorklet1 ;
[SameObject , SecureContext ] readonly attribute Worklet fakeWorklet2 ;
};
每个 Window
都有两个 Worklet
实例,假 Worklet 1 和 假 Worklet 2。这两个的 Worklet 全局作用域类型 都设置为 FakeWorkletGlobalScope
,其 Worklet 目标类型 设置为“fakeworklet
”。用户代理应该为每个 worklet 创建至少两个 FakeWorkletGlobalScope
实例。
“fakeworklet
” 实际上根据 Fetch 并不是有效的 目标。但这说明了真正的 worklets 通常如何拥有自己的 worklet 类型特定的目标。 [FETCH]
fakeWorklet1
获取器步骤是返回 this 的 假 Worklet 1。
fakeWorklet2
获取器步骤是返回 this 的 假 Worklet 2。
[Global =(Worklet ,FakeWorklet ),
Exposed =FakeWorklet ,
SecureContext ]
interface FakeWorkletGlobalScope : WorkletGlobalScope {
undefined registerFake (DOMString type , Function classConstructor );
};
每个 FakeWorkletGlobalScope
都有一个 已注册类构造函数映射,它是一个 有序映射,最初为空。
registerFake(type, classConstructor)
方法步骤是将 this 的 已注册类构造函数映射[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 开发人员对我们假工作线程的预期用途之一是允许他们自定义布尔取反这一高度复杂的过程。他们可能会按如下方式注册其自定义内容
// script.mjs
registerFake( 'negation-processor' , class {
process( arg) {
return ! arg;
}
});
// app.mjs
window. fakeWorklet1. addModule( "script.mjs" );
为了使用此类注册的类,假工作线程的规范可以定义一个查找真值的相反值算法,给定一个Worklet
worklet
可选地,为worklet 创建一个工作线程全局作用域。
令classConstructor为workletGlobalScope的已注册的类构造函数映射["negation-processor
"]。
令classInstance为构造classConstructor的结果,不带任何参数。
令function为Get(classInstance,"process
")。重新抛出任何异常。
返回调用callback并传入« true »和"rethrow
",以及将回调 this 值设置为classInstance的结果。
另一种可能更好的规范架构是在注册时,作为registerFake()
方法步骤的一部分,提取"process
"属性并将其转换为Function
。
WorkletGlobalScope
的子类用于创建全局对象,其中加载到特定Worklet
中的代码可以执行。
[Exposed =Worklet , SecureContext ]
interface WorkletGlobalScope {};
其他规范旨在继承WorkletGlobalScope
,添加注册类的 API,以及特定于其工作线程类型的其他 API。
每个WorkletGlobalScope
都有一个关联的模块映射。它是一个模块映射,最初为空。
本节是非规范性的。
每个WorkletGlobalScope
都包含在其自己的工作线程代理中,该代理具有其对应的事件循环。但是,在实践中,这些代理和事件循环的实现预计将不同于大多数其他代理和事件循环。
每个WorkletGlobalScope
都存在一个工作线程代理,因为理论上,实现可以为每个WorkletGlobalScope
实例使用一个单独的线程,并且允许这种级别的并行性最好使用代理来完成。但是,由于它们的 [[CanBlock]] 值为 false,因此没有要求代理和线程是一对一的。这允许实现自由地在任何线程上执行加载到工作线程中的脚本,包括运行来自其他具有 [[CanBlock]] 值为 false 的代理的代码的线程,例如同源窗口代理(“主线程”)的线程。将其与专用工作线程代理形成对比,后者对 [[CanBlock]] 的真值实际上要求它们获得一个专用的操作系统线程。
工作线程事件循环也有些特殊。它们仅用于与addModule()
关联的任务、用户代理调用作者定义的方法的任务以及微任务。因此,即使事件循环处理模型指定所有事件循环都持续运行,实现也可以使用更简单的策略获得可观察到的等效结果,该策略只是调用作者提供的方法,然后依靠该过程执行微任务检查点。
要为Worklet
worklet创建一个工作线程全局作用域
令outsideSettings为worklet的相关设置对象。
令agent为给定outsideSettings 获取工作线程代理的结果。在该代理中运行这些步骤的其余部分。
令realmExecutionContext为给定agent和以下自定义项创建新的领域的结果
对于全局对象,创建一个由worklet的工作线程全局作用域类型给定的类型的新对象。
令workletGlobalScope为realmExecutionContext的 Realm 组件的全局对象。
令insideSettings为给定realmExecutionContext和outsideSettings 设置工作线程环境设置对象的结果。
令runNextAddedModule为以下步骤
如果pendingAddedModules不为空,则
将workletGlobalScope追加到outsideSettings的全局对象的关联的 Document
的工作线程全局作用域。
运行insideSettings指定的负责的事件循环。
运行runNextAddedModule。
给定WorkletGlobalScope
workletGlobalScope终止工作线程全局作用域
等待eventLoop完成当前正在运行的任务。
销毁eventLoop。
从Worklet
的全局作用域(其全局作用域包含workletGlobalScope)中移除workletGlobalScope。
从Document
的工作线程全局作用域(其工作线程全局作用域包含workletGlobalScope)中移除workletGlobalScope。
给定JavaScript 执行上下文executionContext和环境设置对象outsideSettings设置工作线程环境设置对象
令origin为唯一的不透明来源。
令inheritedAPIBaseURL为outsideSettings的API 基本 URL。
令realm 为executionContext 的 Realm 组件的值。
令workletGlobalScope 为realm 的全局对象。
令settingsObject 为一个新的环境设置对象,其算法定义如下:
返回executionContext。
返回workletGlobalScope 的模块映射。
返回inheritedAPIBaseURL。
与从单个资源派生的 Worker 或其他全局对象不同,Worklet 没有主资源;相反,多个脚本(每个脚本都有自己的 URL)通过worklet.addModule()
加载到全局作用域中。因此,此API 基本 URL 与其他全局对象的 API 基本 URL 存在差异。但是,到目前为止,这并不重要,因为可供 Worklet 代码使用的任何 API 都不使用API 基本 URL。
返回origin。
返回inheritedPolicyContainer。
返回TODO。
将settingsObject 的id 设置为一个新的唯一不透明字符串,创建 URL 设置为inheritedAPIBaseURL,顶级创建 URL 设置为 null,顶级源 设置为outsideSettings 的顶级源,目标浏览上下文 设置为 null,以及活动 Service Worker 设置为 null。
将realm 的 [[HostDefined]] 字段设置为settingsObject。
返回settingsObject。
Worklet
类所有当前引擎都支持。
该Worklet
类提供将模块脚本添加到其关联的WorkletGlobalScope
中的功能。然后,用户代理可以创建在WorkletGlobalScope
上注册的类并调用其方法。
[Exposed =Window , SecureContext ]
interface Worklet {
[NewObject ] Promise <undefined > addModule (USVString moduleURL , optional WorkletOptions options = {});
};
dictionary WorkletOptions {
RequestCredentials credentials = "same-origin";
};
创建Worklet
实例的规范必须为给定实例指定以下内容:
其Worklet 全局作用域类型,它必须是 Web IDL 类型,并且继承自WorkletGlobalScope
;以及
其Worklet 目标类型,它必须是目标,并在获取脚本时使用。
await worklet.addModule(moduleURL[, { credentials }])
所有当前引擎都支持。
将moduleURL 给出的模块脚本 加载并执行到worklet 的所有全局作用域 中。根据 Worklet 类型,它还可以在此过程中创建其他全局作用域。一旦脚本已成功加载并在所有全局作用域中运行,则返回的 Promise 将完成。
可以将credentials
选项设置为凭据模式 以修改脚本获取过程。默认为“same-origin
”。
在获取 脚本或其依赖项时发生的任何错误都将导致返回的 Promise 被拒绝,并带有“AbortError
”DOMException
。在解析脚本或其依赖项时发生的任何错误都将导致返回的 Promise 被拒绝,并带有解析期间生成的异常。
一个Worklet
具有一个列表 的全局作用域,其中包含Worklet
的Worklet 全局作用域类型 的实例。最初为空。
一个Worklet
具有一个已添加模块列表,它是一个列表 的URL,最初为空。对该列表的访问应该是线程安全的。
一个Worklet
具有一个模块响应映射,它是一个有序映射,从URL 到“fetching
”或元组,该元组由响应 和 null、失败或表示响应正文的字节序列 组成。此映射最初为空,并且对它的访问应该是线程安全的。
该已添加模块列表 和模块响应映射 存在是为了确保在不同时间创建的WorkletGlobalScope
获取等效的模块脚本 在其中运行,基于相同的源文本。这使得创建额外的WorkletGlobalScope
对作者来说是透明的。
在实践中,用户代理预计不会使用线程安全编程技术来实现这些数据结构以及咨询它们的操作。相反,当调用addModule()
时,用户代理可以在主线程上获取模块图,并将获取的源文本(即模块响应映射 中包含的重要数据)发送到每个具有WorkletGlobalScope
的线程。
然后,当用户代理创建 给定Worklet
的新的WorkletGlobalScope
时,它可以简单地将获取的源文本映射和来自主线程的入口点列表发送到包含新的WorkletGlobalScope
的线程。
该addModule(moduleURL, options)
方法步骤如下:
令moduleURLRecord 为给定moduleURL(相对于outsideSettings)的编码解析 URL 的结果。
如果moduleURLRecord 为失败,则返回一个被拒绝的 Promise,并带有“SyntaxError
”DOMException
。
令promise 为一个新的 Promise。
并行运行以下步骤in parallel
在继续之前,等待创建过程(包括在Worklet 代理 中进行的过程)的所有步骤完成。
令addedSuccessfully 为 false。
对于每个 workletGlobalScope 的 this 的 全局作用域,在给定 workletGlobalScope 的 网络任务源 上 排队一个全局任务,以给定 moduleURLRecord、outsideSettings、this 的 工作线程目标类型、options["credentials
"]、workletGlobalScope 的 相关设置对象、this 的 模块响应映射 和给定 script 的以下步骤 获取工作线程脚本图
这些获取操作中,只有第一个会真正执行网络请求;其他 WorkletGlobalScope
的获取操作将重用 this 的 模块响应映射 中的 响应。
如果 script 为 null,则
在给定 this 的 相关全局对象 的 网络任务源 上 排队一个全局任务 以执行以下步骤
如果 pendingTasks 不为 −1,则
将 pendingTasks 设置为 −1。
使用 "AbortError
" DOMException
拒绝 promise。
中止这些步骤。
如果 script 的 需要重新抛出的错误 不为 null,则
如果 addedSuccessfully 为 false,则
给定 script 运行模块脚本。
在给定 this 的 相关全局对象 的 网络任务源 上 排队一个全局任务 以执行以下步骤
如果 pendingTasks 不为 −1,则
将 pendingTasks 设置为 pendingTasks − 1。
如果 pendingTasks 为 0,则解决 promise。
返回 promise。
Worklet
的生命周期没有特殊考虑;它与它所属的对象(例如 Window
)绑定。
每个 Document
都有一个 工作线程全局作用域,它是一个 集合,包含 WorkletGlobalScope
,初始为空。
WorkletGlobalScope
的生命周期至少与 Document
绑定,其 工作线程全局作用域 包含它。特别是,销毁 Document
将 终止 对应的 WorkletGlobalScope
并允许其被垃圾回收。
此外,用户代理可以随时 终止 给定的 WorkletGlobalScope
,除非定义相应工作线程类型的规范另有说明。例如,如果 工作线程代理 的 事件循环 没有排队的 任务,或者用户代理没有计划使用该工作线程的挂起操作,或者用户代理检测到异常操作(如无限循环或回调超过规定的时间限制),它们可能会终止它们。
最后,特定工作线程类型的规范可以提供有关何时 创建 给定工作线程类型的 WorkletGlobalScope
的更多具体细节。例如,它们可能在调用工作线程代码的特定过程中创建它们,如 示例 中所示。