简单看了下chromium文档中关于worker的除基本使用之外的部分内容。

  • worker类型
  • 生命周期
  • 进程与线程模型
  • 资源处理

官方设计文档的优点是只言片语就能描述出核心内容。

Blink中的Web Workers

Web Worker是一个Web特性,该特性提供一个后台JS上下文并允许在后台运行任意脚本,通常运行在与主UI线程分离的一个线程中。

HTML5 Workers: Dedicated Worker 与 Shared Worker

Dedicated worker 与 shared worker是两种"标准"worker,作为HTML5标准中的一部分,其他种类的worker都是基于这些worker设计的。

Dedicated worker 或直接称做“worker”,是最基础的worker,它会在与创建者文档绑定的后台上下文中,执行一个脚本。一个worker的生命周期基本上与其创建者的文档是一致的。

Shared worker 通过脚本的URL标识,可以由运行在同一域下的多个文档(站点)共享。当一个文档调用SharedWorker的构造器时,它会创建一个新的worker或连接至一个已存在的worker(若相同URL脚本的worker已经在运行)。worker上下文会在至少存在一个活跃文档的情况下保持存活。

Service Workers

Service worker是一种强大的Web平台新特性,它允许脚本作为一个网络代理在后台运行,比如拦截一系列关联文档的请求。

service worker有一点类似shared worker,它们都可以被运行在同一域下的多个文档共享,但service worker的生命周期并不与任何文档绑定。

service worker以事件驱动的方式执行任务,即无论worker何时接收到事件,它都会启动(在之后被终止)

“Nested” Workers

即嵌套worker。HTML5 Worker(或其他worker,如service worker)可以由另一个worker创建与初始化,这些worker被称为嵌套worker(nested workers)。在2015年10月(ps:该文档撰写的时间)的Chromium/Blink中是不支持的(tracking bug: crbug.com/31666)。

根据文档中的描述,一个负责worker上下文的js上下文(比如一个worker的创建者文档)会被简记为父级文档关联文档,这个js上下文也可以是一个worker(在嵌套worker中的情况)。

术语

worker上下文

worker上下文 通常是指一个后台的JS上下文,即worker运行的地方。一般worker上下文会运行在一个与主线程不同的线程,被称做“worker线程”。

worker线程

worker线程 指的是worker的js上下文(或称worker上下文)运行所在的线程。当一个新worker被创建时则worker线程即被创建,当worker关闭或被干掉时线程也会关闭。

worker线程有时也指Blink中WorkerThread类,该者通常对应一个底层平台的线程(由Blink中的WebThread表示),而有时则不是(比如compositor worker的情况)。下面会介绍worker不同的线程模型。

worker对象

worker对象 通常是指一个JS的“Worker”对象,通过该对象实现worker与其关联文档的通信。需要注意的事,worker对象是在worker的父级上下文(或关联上下文)中初始化的,这意味着它在父级上下文的线程中进行活动,该线程在多数情况下是主线程。

worker全局域

worker全局域 指的是一个worker js上下文的全局作用域(window是document中的JS全局域),worker全局域在Web Worker标准中被定义为WorkerGlobalScope接口。

WorkerGlobalScope接口暴露出了可用的Web worker API,但并不是所有的window域下的API在worker全局域都是可用的,有些API仅在worker中可用(Web Worker可用API列表)。

关系

下面这张图表现出了worker上下文、worker线程、worker对象与worker全局念之间的关系:

生命周期

虽然不同类型的worker具有不同的生命周期及进程模型,不过一般都是以下面的过程启动的:

  1. 文档创建一个新的worker对象,来实例化一个新的worker
  2. 接着worker对象会加载worker脚本
  3. 一旦脚本加载完成,则一个新的worker线程会被创建,该线程的主函数会配置一个新的worker全局作用域作为worker脚本的全局作用域
  4. worker线程会启动循环,并注入脚本

对于不同类型的worker其终止行为也不同,当发现下列情况时HTML5 worker会被终止:

  • worker显式地调用WorkerGlobalScope#close方法
  • 父级文档中调用Worker#terminate方法(仅对于dedicated worker)
  • 所有关联文档均关闭或变为不活跃状态,或:
  • 若文档切断了与worker对象的引用,则worker上下文中将不存在任何等待的活动,并且GC会启动并收集该worker对象。

除了第一种方法(WorkerGlobalScope#close)外,其他终止行为均由主线程触发,因此需要停止worker线程中的执行。

更多的有关生命周期的规范可以参考标准文档

进程与线程模型

进程模型 线程模型
Dedicated Worker 进程内 运行在自身线程 (Worker上下文 : 线程数 = 1:1)
Shared Worker 进程外 运行在自身线程 (Worker上下文 : 线程数 = 1:1)
Service Worker 进程外 运行在自身线程 (Worker上下文 : 线程数 = 1:1)

进程模型(Process Model)

Worker可以根据进程模型粗糙的分为两类: 进程内(in-process)worker与进程外(out-of-process)worker。

  • 进程内worker运行在与它们关联文档所在的同一进程中,因此它们仅需在文档中单纯的"添加新的线程"
  • 进程外worker可能运行在一个与他们关联文档不同的进程中。通常若一个worker需要由多个文档共享时,由于处在不同进程的不同文档均需要与同一个worker通信,Blink/Chromium一般会将其实现为一个进程外worker

在实现方面,进程内worker与它们文档的通信可以在渲染进程中通过在worker线程与主线程之间上传任务来实现。而进程外worker则需要使用IPC来与它们的文档通信,不管这个worker是否运行在同一还是不同的进程中。

线程模型(Thread Model)

几乎所有的worker都运行在它们自己的线程中(worker上下文 : worker线程 = 1:1的关系),不过也有例外: Compositor Worker运行在一个上下文无关、与进程对应的单例线程中,因此其worker上下文与worker线程关系为N:1。而Houdini Worker在计划中是与文档运行在同一线程(即worker与文档共享同一线程)

非主线程资源拉取(Off-the-main-thread fetch)

所有worker子资源与一些顶级脚本,均在非主线程(比如worker/worklet线程)中拉取。

在worker/worklet线程中存在两种网络资源fetch方式: insideSetting fetch与outsideSettings fetch。术语insideSettings与outsideSettings来自HTMLWorklet标准。

insideSettings fetch

insideSettings fetch指子资源拉取。

在标准中,insideSettings对应WorkerOrWorkletGlobalScope中的worker环境配置对象

在实现中,insideSettings直接对应了WorkerOrWorkletGlobalScope。WorkerOrWorkletGlobalScope::Fetcher()对应WorkerFetchContext,使用WorkerOrWorkletGlobalScope::GetContentSecurityPolicy()。

目前所有的子资源拉取均为非主线程行为。

outsideSettings fetch

outsideSettings fetch指非主线程的顶级worker/worklet脚本拉取。

在标准中,outsideSettings为worker父级上下文的环境配置对象。

在实现中,outsideSettings应该对应文档(或嵌套worker的WorkerOrWorkletGlobalScope),但worker线程由于线程限制无法访问这些对象,因此我们会传递一个包含这些跨线程信息的快照FetchClientSettingsObjectSnapshot,并在worker线程中创建ResourceFetcher, WorkerFetchContext和ContentSecurityPolicy(分离在insideSettings fetch中使用的对象)。他们的行为类似父级上下文,通过WorkerOrWorkletGlobalScope::CreateOutsideSettingsFetcher()使用。

注意,在非主线程拉取不可用的场景下(如classic worker),worker脚本会在主线程拉取,因此不会牵扯到WorkerOrWorkletGlobalScope与worker线程。

在Worker中使用ES模块

目前已知只有chrome的80版本后的dedicated worker中支持该特性。

有三种方式可以在dedicated worker中使用ES模块

  1. Worker构造函数

该方法在Worker构造器中的第二个参数设置type选项即可。将type设置为’module’后,worker的顶级脚本(top-level srcipt, 即最先执行的脚本)会被作为Module Script执行。

<script>
const worker = new Worker('module-worker.js', { type: 'module' });
</script>

在上面的例子中,就不在/标签中设置type='module'也没有问题。

  1. 静态导入(static import)

第二种是在worker内的静态导入方法

// module-worker.js
import * as module from './hello-module.js';
module.SayHello();

由于静态导入只能在Module Script中使用,因此需要结合第一种方法,将Module Script作为worker顶级脚本启动。下面是一个将Classic Script作为顶级脚本启动而静态导入失败的例子,当worker启动时自身会报错。

<script>
// Worker会被当做普通脚本执行
const worker = new Worker('module-worker.js');
worker.onerror = e => {
  // 这里会触发错误事件处理器
};
</script>
  1. 动态导入(dynamic import)

第三种是在worker内的动态导入方法

// In module-worker.js
import('./hello-module.js')
  .then(module => module.SayHello());

动态导入在作为Classic Script启动的worker内也可以使用

<script>
const worker = new Worker('module-worker.js');
worker.onerror = e => {
  // 这里不会触发错误事件处理器
};
</script>

其他

行内脚本的使用方式

针对dedicated worker

<script id="my_worker" type="javascript/worker">
self.addEventListener("message", e => {
    self.postMessage(`${e.data} from worker!`);
});
</script>
const blob = new Blob([document.querySelector('#my_worker').textContent]);
const worker = new Worker(window.URL.createObjectURL(blob));
worker.addEventListener("message", e => {
    console.log("Received: " + e.data);
});
worker.postMessage("hello");

参考