Blink Worker纸上谈兵
简单看了下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具有不同的生命周期及进程模型,不过一般都是以下面的过程启动的:
- 文档创建一个新的worker对象,来实例化一个新的worker
- 接着worker对象会加载worker脚本
- 一旦脚本加载完成,则一个新的worker线程会被创建,该线程的主函数会配置一个新的worker全局作用域作为worker脚本的全局作用域
- 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来自HTML与Worklet标准。
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模块
- Worker构造函数
该方法在Worker构造器中的第二个参数设置type选项即可。将type设置为’module’后,worker的顶级脚本(top-level srcipt, 即最先执行的脚本)会被作为Module Script执行。
<script>
const worker = new Worker('module-worker.js', { type: 'module' });
</script>
在上面的例子中,就不在/标签中设置type='module'
也没有问题。
- 静态导入(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>
- 动态导入(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");
参考
- Blink Workers (Jul 2016)
- Design of UseCounter for workers (Feb 14, 2017)
- Worker / Worklet Internals (April 19, 2018)
- ES Modules for Dedicated Workers (Mar 8, 2018)
- WorkerGlobalScope Initialization (April 1, 2019)
- ES Modules for Shared Workers (Feb 19, 2020)
- JavaScript のスレッド並列実行環境
- Chrome 80 から Web Worker (Dedicated Worker) で ES Modules が使えます
- 原文作者:yrq110
- 原文链接:http://yrq110.me/post/front-end/contents-about-blink-web-worker/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。