[TOC]

27.1 工作者线程简介

JavaScript环境实际上是运行在托管操作系统中的虚拟环境。在浏览器中每打开一个页面,就会分配一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM,等等。每个页面就相当于一个沙盒,不会干扰其他页面。对于浏览器来说,同时管理多个环境是非常简单的,因为所有这些环境都是并行执行的。
使用工作者线程,浏览器可以在原始页面环境之外,再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的API(如DOM)互操作,但可以与父环境并行执行代码。

27.1.1 工作者线程与线程

作为介绍,通常需要将工作者线程与执行线程进行比较。
在许多方面,这是一个恰当的比较,因为工作者线程和线程确实有很多共同之处。
❑ 工作者线程是以实际线程实现的。例如,Blink浏览器引擎实现工作者线程的WorkerThread就对应着底层的线程。
❑ 工作者线程并行执行。虽然页面和工作者线程都是单线程JavaScript环境,每个环境中的指令则可以并行执行。
❑ 工作者线程可以共享某些内存。工作者线程能够使用SharedArrayBuffer在多个环境间共享内容。虽然线程会使用锁实现并发控制,但JavaScript使用Atomics接口实现并发控制。
工作者线程与线程有很多类似之处,但也有重要的区别
❑ 工作者线程不共享全部内存。在传统线程模型中,多线程有能力读写共享内存空间。除了Shared-ArrayBuffer外,从工作者线程进出的数据需要复制或转移。
❑ 工作者线程不一定在同一个进程里。通常,一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于。例如,Chrome的Blink引擎对共享工作者线程和服务工作者线程使用独立的进程。
❑ 创建工作者线程的开销更大。工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他JavaScript环境必需的特性。创建这些结构的代价不容忽视。
无论形式还是功能,工作者线程都不是用于替代线程的。

27.1.2 工作者线程的类型

Web工作者线程规范中定义了三种主要的工作者线程:
专用工作者线程、共享工作者线程和服务工作者线程。
现代浏览器都支持这些工作者线程。

1.专用工作者线程

专用工作者线程,通常简称为工作者线程、Web Worker或Worker,是一种实用的工具,可以让脚本单独创建一个JavaScript线程,以执行委托的任务。专用工作者线程,顾名思义,只能被创建它的页面使用。

2.共享工作者线程

共享工作者线程与专用工作者线程非常相似。主要区别是:共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。

3.服务工作者线程

服务工作者线程与专用工作者线程和共享工作者线程截然不同。它的主要用途是:拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。

27.1.3 WorkerGlobalScope

在网页上,window对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有window的概念。这里的全局对象是WorkerGlobalScope的实例,通过self关键字暴露出来。

1.WorkerGlobalScope属性和方法

self上可用的属性是window对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本。

2.WorkerGlobalScope的子类

实际上并不是所有地方都实现了WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这继承自WorkerGlobalScope。

27.2 专用工作者线程

专用工作者线程是最简单的Web工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。

27.2.1 专用工作者线程的基本概念

可以把专用工作者线程称为后台脚本(background script)。
JavaScript线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。
该脚本也可以再请求其他脚本,但一个线程总是从一个脚本源开始。

1.创建专用工作者线程

创建专用工作者线程最常见的方式是:加载JavaScript文件。把文件路径提供给Worker构造函数,然后构造函数再在后台异步加载脚本,并实例化工作者线程。传给构造函数的文件路径可以是多种形式。

2.工作者线程安全限制

工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误。
基于加载脚本创建的工作者线程不受文档的内容安全策略限制,因为工作者线程在与父文档不同的上下文中运行。不过,如果工作者线程加载的脚本带有全局唯一标识符(与加载自一个二进制大文件一样),就会受父文档内容安全策略的限制。

3.使用Worker对象

Worker()构造函数返回的Worker对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。
注:要管理好使用Worker()创建的每个Worker对象。在终止工作者线程之前,它不会被垃圾回收,也不能通过编程方式恢复对之前Worker对象的引用。
Worker对象支持下列事件处理程序属性:
❑ onerror:在工作者线程中发生ErrorEvent类型的错误事件时会调用指定给该属性的处理程序。
❑ onmessage:在工作者线程中发生MessageEvent类型的消息事件时会调用指定给该属性的处理程序。
❑ onmessageerror:在工作者线程中发生MessageEvent类型的错误事件时会调用指定给该属性的处理程序。
Worker对象还支持下列方法:
❑ postMessage():用于通过异步消息事件向工作者线程发送信息。
❑ terminate():用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。

4.DedicatedWorkerGlobalScope

在专用工作者线程内部,全局作用域是DedicatedWorkerGlobalScope的实例。
因为这继承自WorkerGlobalScope,所以包含它的所有属性和方法。
工作者线程可以通过self关键字访问该全局作用域。
DedicatedWorkerGlobalScope在WorkerGlobalScope基础上增加了以下属性和方法:
❑ name:可以提供给Worker构造函数的一个可选的字符串标识符。
❑ postMessage():与worker.postMessage()对应的方法,用于从工作者线程内部向父上下文发送消息。
❑ close():与worker.terminate()对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。
❑ importScripts():用于向工作者线程中导入任意数量的脚本。

27.2.2 专用工作者线程与隐式MessagePorts

专用工作者线程的Worker对象和DedicatedWorkerGlobalScope与MessagePorts有一些相同接口处理程序和方法:onmessage、onmessageerror、close()和postMessage()。
这不是偶然的,因为专用工作者线程隐式使用了MessagePorts在两个上下文之间通信。
也有不一致的地方,比如start()和close()约定。
专用工作者线程会自动发送排队的消息,因此start()也就没有必要了。
另外,close()在专用工作者线程的上下文中没有意义,因为这样关闭MessagePort会使工作者线程孤立。
因此,在工作者线程内部调用close()(或在外部调用terminate())不仅会关闭MessagePort,也会终止线程。

27.2.3 专用工作者线程的生命周期

一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始化(initializing)、活动(active)和终止(terminated)。
自我终止和外部终止最终都会执行相同的工作者线程终止例程。
工作者线程不需要执行同步停止。
在整个生命周期中,一个专用工作者线程只会关联一个网页(Web工作者线程规范称其为一个文档)。除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或关闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。

27.2.4 配置Worker选项

Worker()构造函数允许将可选的配置对象作为第二个参数。该配置对象支持下列属性:name, type, credentials

27.2.5 在JavaScript行内创建工作者线程

工作者线程需要基于脚本文件来创建,但这并不意味着该脚本必须是远程资源。
专用工作者线程也可通过Blob对象URL在行内脚本创建。这样可以更快速地初始化工作者线程,因为没有网络延迟。
工作者线程也可以利用函数序列化来初始化行内脚本。这是因为函数的toString()方法返回函数代码的字符串,而函数可以在父上下文中定义但在子上下文中执行。

27.2.6 在工作者线程中动态执行脚本

工作者线程中的脚本可用importScripts()方法,通过编程方式加载和执行任意脚本。
该方法可用于全局Worker对象。这个方法会加载脚本并按照加载顺序同步执行。
importScripts()方法可以接收任意数量的脚本作为参数。浏览器下载它们的顺序没有限制,但执行则会严格按照它们在参数列表的顺序进行。
脚本加载受到常规CORS的限制,但在工作者线程内部可以请求来自任何源的脚本。这里的脚本导入策略类似于使用生成的