1. 11 Worklets
    1. 11.1 简介
      1. 11.1.1 动机
      2. 11.1.2 代码幂等性
      3. 11.1.3 推测性评估
    2. 11.2 示例
      1. 11.2.1 加载脚本
      2. 11.2.2 注册类并调用其方法
    3. 11.3 基础设施
      1. 11.3.1 全局作用域
        1. 11.3.1.1 代理和事件循环
        2. 11.3.1.2 创建和终止
        3. 11.3.1.3 Worklets 的脚本设置
      2. 11.3.2 Worklet
      3. 11.3.3 Worklet 的生命周期

11 Worklets

11.1 简介

本节是非规范性的。

Worklets 是一部分规范基础设施,可用于运行独立于主 JavaScript 执行环境的脚本,同时不需要任何特定的实现模型。

此处指定的 worklet 基础设施无法被 Web 开发人员直接使用。相反,其他规范基于它创建直接可用的 worklet 类型,专门用于在浏览器实现管道的特定部分运行。

11.1.1 动机

本节是非规范性的。

允许扩展渲染或实现管道其他敏感部分(如音频输出)的扩展点非常困难。如果扩展点使用对 Window 上提供的 API 的完全访问权限来完成,引擎将需要放弃之前对这些阶段中间可能发生的事情所做的假设。例如,在布局阶段,渲染引擎假设不会修改 DOM。

此外,在 Window 环境中定义扩展点会限制用户代理在与 Window 对象相同的线程中执行工作。(除非实现添加了复杂的高开销基础设施以允许线程安全 API 以及线程连接保证。)

Worklets 旨在允许扩展点,同时保持用户代理当前依赖的保证。这是通过基于 WorkletGlobalScope 的子类的新的全局环境来完成的。

Worklets 类似于 Web Workers。但是,它们

由于 worklets 开销相对较高,因此最好谨慎使用它们。因此,预计给定的 WorkletGlobalScope 将在多个单独的脚本之间共享。(这类似于单个 Window 如何在多个单独的脚本之间共享。)

Worklets 是一种通用技术,可用于不同的用例。一些 worklets,例如在 CSS 绘制 API 中定义的那些,提供了旨在用于无状态、幂等和短运行计算的扩展点,这些扩展点具有下一节中描述的特殊注意事项。其他一些,例如在 Web 音频 API 中定义的那些,用于有状态、长时间运行的操作。 [CSSPAINT] [WEBAUDIO]

11.1.2 代码幂等性

一些使用 worklets 的规范旨在允许用户代理在多个线程上并行化工作,或根据需要在线程之间移动工作。在这些规范中,用户代理可能会以 实现定义 的顺序调用 Web 开发人员提供的类上的方法。

因此,为了防止互操作性问题,在这样的 WorkletGlobalScope 上注册类的作者应该使他们的代码幂等。也就是说,给定特定输入,类上的方法或一组方法应该产生相同的输出。

此规范使用以下技术来鼓励作者以幂等的方式编写代码

总之,这些限制有助于防止两个不同的脚本使用 全局对象 的属性共享状态。

此外,使用 worklets 并打算允许 实现定义 行为的规范必须遵守以下规定

11.1.3 推测性评估

一些使用 worklets 的规范可以根据用户代理的状态调用 Web 开发人员提供的类上的方法。为了提高线程之间的并发性,用户代理可能会根据潜在的未来状态推测性地调用方法。

在这些规范中,用户代理可能会随时调用此类方法,并使用任何参数,而不仅仅是对应于用户代理当前状态的参数。此类推测性评估的结果不会立即显示,但可以缓存以供使用,如果用户代理状态与推测的状态匹配。这可以提高用户代理和 worklet 线程之间的并发性。

因此,为了防止用户代理之间的互操作性风险,在这样的 WorkletGlobalScope 上注册类的作者应该使他们的代码无状态。也就是说,调用方法的唯一效果应该是其结果,而不是任何副作用,例如更新可变状态。

鼓励 代码幂等性 的相同技术也鼓励作者编写无状态代码。

11.2 示例

本节是非规范性的。

对于这些示例,我们将使用一个假的 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

11.2.1 加载脚本

本节是非规范性的。

要将脚本加载到 假 Worklet 1 中,Web 开发人员会编写

window.fakeWorklet1.addModule('script1.mjs');
window.fakeWorklet1.addModule('script2.mjs');

请注意,哪个脚本先完成获取和运行取决于网络时序:可能是 script1.mjsscript2.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!

11.2.2 注册类并调用其方法

本节是非规范性的。

假设 Web 开发人员对我们假工作线程的预期用途之一是允许他们自定义布尔取反这一高度复杂的过程。他们可能会按如下方式注册其自定义内容

// script.mjs
registerFake('negation-processor', class {
  process(arg) {
    return !arg;
  }
});
// app.mjs
window.fakeWorklet1.addModule("script.mjs");

为了使用此类注册的类,假工作线程的规范可以定义一个查找真值的相反值算法,给定一个Worklet worklet

  1. 可选地,为worklet 创建一个工作线程全局作用域

  2. workletGlobalScopeworklet全局作用域之一,以实现定义的方式选择。

  3. classConstructorworkletGlobalScope已注册的类构造函数映射["negation-processor"]。

  4. classInstance构造classConstructor的结果,不带任何参数。

  5. functionGet(classInstance,"process")。重新抛出任何异常。

  6. callback转换function为 Web IDL Function实例的结果。

  7. 返回调用callback并传入« true »和"rethrow",以及将回调 this 值设置为classInstance的结果。

另一种可能更好的规范架构是在注册时,作为registerFake()方法步骤的一部分,提取"process"属性并将其转换为Function

11.3 基础设施

11.3.1 全局作用域

WorkletGlobalScope的子类用于创建全局对象,其中加载到特定Worklet中的代码可以执行。

[Exposed=Worklet, SecureContext]
interface WorkletGlobalScope {};

其他规范旨在继承WorkletGlobalScope,添加注册类的 API,以及特定于其工作线程类型的其他 API。

每个WorkletGlobalScope都有一个关联的模块映射。它是一个模块映射,最初为空。

11.3.1.1 代理和事件循环

本节是非规范性的。

每个WorkletGlobalScope都包含在其自己的工作线程代理中,该代理具有其对应的事件循环。但是,在实践中,这些代理和事件循环的实现预计将不同于大多数其他代理和事件循环。

每个WorkletGlobalScope都存在一个工作线程代理,因为理论上,实现可以为每个WorkletGlobalScope实例使用一个单独的线程,并且允许这种级别的并行性最好使用代理来完成。但是,由于它们的 [[CanBlock]] 值为 false,因此没有要求代理和线程是一对一的。这允许实现自由地在任何线程上执行加载到工作线程中的脚本,包括运行来自其他具有 [[CanBlock]] 值为 false 的代理的代码的线程,例如同源窗口代理(“主线程”)的线程。将其与专用工作线程代理形成对比,后者对 [[CanBlock]] 的真值实际上要求它们获得一个专用的操作系统线程。

工作线程事件循环也有些特殊。它们仅用于与addModule()关联的任务、用户代理调用作者定义的方法的任务以及微任务。因此,即使事件循环处理模型指定所有事件循环都持续运行,实现也可以使用更简单的策略获得可观察到的等效结果,该策略只是调用作者提供的方法,然后依靠该过程执行微任务检查点

11.3.1.2 创建和终止

要为Worklet worklet创建一个工作线程全局作用域

  1. outsideSettingsworklet相关设置对象

  2. agent为给定outsideSettings 获取工作线程代理的结果。在该代理中运行这些步骤的其余部分。

  3. realmExecutionContext为给定agent和以下自定义项创建新的领域的结果

  4. workletGlobalScoperealmExecutionContext的 Realm 组件的全局对象

  5. insideSettings为给定realmExecutionContextoutsideSettings 设置工作线程环境设置对象的结果。

  6. pendingAddedModulesworklet已添加模块列表克隆

  7. runNextAddedModule为以下步骤

    1. 如果pendingAddedModules不为空,则

      1. moduleURL为从pendingAddedModules出队的结果。

      2. 给定moduleURLinsideSettingsworklet工作线程目标类型什么凭据模式?insideSettingsworklet模块响应映射 获取工作线程脚本图,以及给定script的以下步骤

        这实际上不会执行网络请求,因为它只会重用worklet模块响应映射中的响应。此步骤的主要目的是从响应创建一个新的特定于workletGlobalScope模块脚本

        1. 断言script不为 null,因为获取成功并且源文本在worklet模块响应映射最初使用moduleURL填充时已成功解析。

        2. 给定script运行模块脚本

        3. 运行runNextAddedModule

      3. 中止这些步骤。
    2. workletGlobalScope追加outsideSettings全局对象关联的 Document工作线程全局作用域

    3. workletGlobalScope追加worklet全局作用域

    4. 运行insideSettings指定的负责的事件循环

  8. 运行runNextAddedModule

给定WorkletGlobalScope workletGlobalScope终止工作线程全局作用域

  1. eventLoopworkletGlobalScope相关代理事件循环

  2. 如果eventLoop任务队列中排队了任何任务,则将其丢弃而不进行处理。

  3. 等待eventLoop完成当前正在运行的任务

  4. 如果上一步在实现定义的时间段内未完成,则中止当前在工作线程中运行的脚本。

  5. 销毁eventLoop

  6. Worklet全局作用域(其全局作用域包含workletGlobalScope)中移除workletGlobalScope

  7. Document工作线程全局作用域(其工作线程全局作用域包含workletGlobalScope)中移除workletGlobalScope

11.3.1.3 工作线程的脚本设置

给定JavaScript 执行上下文executionContext环境设置对象outsideSettings设置工作线程环境设置对象

  1. origin为唯一的不透明来源

  2. inheritedAPIBaseURLoutsideSettingsAPI 基本 URL

  3. inheritedPolicyContaineroutsideSettings克隆 策略容器

  4. realmexecutionContext 的 Realm 组件的值。

  5. workletGlobalScoperealm全局对象

  6. settingsObject 为一个新的环境设置对象,其算法定义如下:

    Realm 执行上下文

    返回executionContext

    模块映射

    返回workletGlobalScope模块映射

    API 基本 URL

    返回inheritedAPIBaseURL

    与从单个资源派生的 Worker 或其他全局对象不同,Worklet 没有主资源;相反,多个脚本(每个脚本都有自己的 URL)通过worklet.addModule() 加载到全局作用域中。因此,此API 基本 URL 与其他全局对象的 API 基本 URL 存在差异。但是,到目前为止,这并不重要,因为可供 Worklet 代码使用的任何 API 都不使用API 基本 URL

    返回origin

    策略容器

    返回inheritedPolicyContainer

    跨源隔离功能

    返回TODO

    时间源

    断言:此算法永远不会被调用,因为时间源 在 Worklet 上下文中不可用。

  7. settingsObjectid 设置为一个新的唯一不透明字符串,创建 URL 设置为inheritedAPIBaseURL顶级创建 URL 设置为 null,顶级源 设置为outsideSettings顶级源目标浏览上下文 设置为 null,以及活动 Service Worker 设置为 null。

  8. realm 的 [[HostDefined]] 字段设置为settingsObject

  9. 返回settingsObject

11.3.2 Worklet

Worklet

所有当前引擎都支持。

Firefox76+Safari14.1+Chrome65+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

Worklet 类提供将模块脚本添加到其关联的WorkletGlobalScope 中的功能。然后,用户代理可以创建在WorkletGlobalScope 上注册的类并调用其方法。

[Exposed=Window, SecureContext]
interface Worklet {
  [NewObject] Promise<undefined> addModule(USVString moduleURL, optional WorkletOptions options = {});
};

dictionary WorkletOptions {
  RequestCredentials credentials = "same-origin";
};

创建Worklet 实例的规范必须为给定实例指定以下内容:

await worklet.addModule(moduleURL[, { credentials }])

Worklet/addModule

所有当前引擎都支持。

Firefox76+Safari14.1+Chrome65+
Opera?Edge79+
Edge (Legacy)?Internet Explorer不支持
Firefox Android?Safari iOS?Chrome Android?WebView Android?Samsung Internet?Opera Android?

moduleURL 给出的模块脚本 加载并执行到worklet 的所有全局作用域 中。根据 Worklet 类型,它还可以在此过程中创建其他全局作用域。一旦脚本已成功加载并在所有全局作用域中运行,则返回的 Promise 将完成。

可以将credentials 选项设置为凭据模式 以修改脚本获取过程。默认为“same-origin”。

获取 脚本或其依赖项时发生的任何错误都将导致返回的 Promise 被拒绝,并带有AbortErrorDOMException。在解析脚本或其依赖项时发生的任何错误都将导致返回的 Promise 被拒绝,并带有解析期间生成的异常。

一个Worklet 具有一个列表全局作用域,其中包含WorkletWorklet 全局作用域类型 的实例。最初为空。

一个Worklet 具有一个已添加模块列表,它是一个列表URL,最初为空。对该列表的访问应该是线程安全的。

一个Worklet 具有一个模块响应映射,它是一个有序映射,从URL 到“fetching”或元组,该元组由响应 和 null、失败或表示响应正文的字节序列 组成。此映射最初为空,并且对它的访问应该是线程安全的。

已添加模块列表模块响应映射 存在是为了确保在不同时间创建的WorkletGlobalScope 获取等效的模块脚本 在其中运行,基于相同的源文本。这使得创建额外的WorkletGlobalScope 对作者来说是透明的。

在实践中,用户代理预计不会使用线程安全编程技术来实现这些数据结构以及咨询它们的操作。相反,当调用addModule() 时,用户代理可以在主线程上获取模块图,并将获取的源文本(即模块响应映射 中包含的重要数据)发送到每个具有WorkletGlobalScope 的线程。

然后,当用户代理创建 给定Worklet 的新的WorkletGlobalScope 时,它可以简单地将获取的源文本映射和来自主线程的入口点列表发送到包含新的WorkletGlobalScope 的线程。

addModule(moduleURL, options) 方法步骤如下:

  1. outsideSettings相关设置对象this

  2. moduleURLRecord 为给定moduleURL(相对于outsideSettings)的编码解析 URL 的结果。

  3. 如果moduleURLRecord 为失败,则返回一个被拒绝的 Promise,并带有SyntaxErrorDOMException

  4. promise 为一个新的 Promise。

  5. 并行运行以下步骤in parallel

    1. 如果this全局作用域为空,则

      1. 创建 Worklet 全局作用域,给定this

      2. 可选地,创建其他全局作用域实例,给定this,具体取决于所讨论的特定 Worklet 及其规范。

      3. 在继续之前,等待创建过程(包括在Worklet 代理 中进行的过程)的所有步骤完成。

    2. pendingTasksthis全局作用域大小

    3. addedSuccessfully 为 false。

    4. 对于每个 workletGlobalScopethis全局作用域,在给定 workletGlobalScope网络任务源排队一个全局任务,以给定 moduleURLRecordoutsideSettingsthis工作线程目标类型options["credentials"]、workletGlobalScope相关设置对象this模块响应映射 和给定 script 的以下步骤 获取工作线程脚本图

      这些获取操作中,只有第一个会真正执行网络请求;其他 WorkletGlobalScope 的获取操作将重用 this模块响应映射 中的 响应

      1. 如果 script 为 null,则

        1. 在给定 this相关全局对象网络任务源排队一个全局任务 以执行以下步骤

          1. 如果 pendingTasks 不为 −1,则

            1. pendingTasks 设置为 −1。

            2. 使用 "AbortError" DOMException 拒绝 promise

        2. 中止这些步骤。

      2. 如果 script需要重新抛出的错误 不为 null,则

        1. 在给定 this相关全局对象网络任务源排队一个全局任务 以执行以下步骤

          1. 如果 pendingTasks 不为 −1,则

            1. pendingTasks 设置为 −1。

            2. 使用 script需要重新抛出的错误 拒绝 promise

        2. 中止这些步骤。

      3. 如果 addedSuccessfully 为 false,则

        1. moduleURLRecord 追加this已添加模块列表

        2. addedSuccessfully 设置为 true。

      4. 给定 script 运行模块脚本

      5. 在给定 this相关全局对象网络任务源排队一个全局任务 以执行以下步骤

        1. 如果 pendingTasks 不为 −1,则

          1. pendingTasks 设置为 pendingTasks − 1。

          2. 如果 pendingTasks 为 0,则解决 promise

  6. 返回 promise

11.3.3 工作线程的生命周期

Worklet 的生命周期没有特殊考虑;它与它所属的对象(例如 Window)绑定。

每个 Document 都有一个 工作线程全局作用域,它是一个 集合,包含 WorkletGlobalScope,初始为空。

WorkletGlobalScope 的生命周期至少与 Document 绑定,其 工作线程全局作用域 包含它。特别是,销毁 Document终止 对应的 WorkletGlobalScope 并允许其被垃圾回收。

此外,用户代理可以随时 终止 给定的 WorkletGlobalScope,除非定义相应工作线程类型的规范另有说明。例如,如果 工作线程代理事件循环 没有排队的 任务,或者用户代理没有计划使用该工作线程的挂起操作,或者用户代理检测到异常操作(如无限循环或回调超过规定的时间限制),它们可能会终止它们。

最后,特定工作线程类型的规范可以提供有关何时 创建 给定工作线程类型的 WorkletGlobalScope 的更多具体细节。例如,它们可能在调用工作线程代码的特定过程中创建它们,如 示例 中所示。