工作者线程
前端开发者常说:“JavaScript 是单线程的。”这种说法虽然有些简单,但描述了 JavaScript 在浏览器中的一般行为。因此,作为帮助 Web 开发人员理解 JavaScript 的教学工具,它非常有用。
单线程就意味着不能像多线程语言那样把工作委托给独立的线程或进程去做。JavaScript 的单线程可以保证它与不同浏览器 API 兼容。假如 JavaScript 可以多线程执行并发更改,那么像 DOM 这样的 API 就会出现问题。因此,POSIX 线程或 Java 的 Thread 类等传统并发结构都不适合 JavaScript。
而这也正是工作者线程的价值所在:允许把主线程的工作转嫁给独立的实体,而不会改变现有的单线程模型。虽然本章要介绍的各种工作者线程有不同的形式和功能,但它们的共同的特点是都独立于 JavaScript 的主执行环境。
工作者线程简介
JavaScript 环境实际上是运行在托管操作系统中的虚拟环境。在浏览器中每打开一个页面,就会分配 一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM,等等。每个页面就相当于一个沙盒,不会干扰其他页面。对于浏览器来说,同时管理多个环境是非常简单的,因为所有这些环境都是并行执行的。
使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码。
工作者线程与线程
作为介绍,通常需要将工作者线程与执行线程进行比较。在许多方面,这是一个恰当的比较,因为工作者线程和线程确实有很多共同之处。
工作者线程相对比较重,不建议大量使用。例如,对一张 400 万像素的图片,为每个像素都启动一个工作者线程是不合适的。通常,工作者线程应该是长期运行的,启动成本比较高, 每个实例占用的内存也比较大。
工作者线程的类型
Web 工作者线程规范中定义了三种主要的工作者线程:专用工作者线程、共享工作者线程和服务工作者线程。现代浏览器都支持这些工作者线程。
- 专用工作者线程
专用工作者线程,通常简称为工作者线程、Web Worker 或 Worker,是一种实用的工具,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务。专用工作者线程,顾名思义,只能被创建它的页面使用。
- 共享工作者线程
共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程 的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。
- 服务工作者线程
服务工作者线程与专用工作者线程和共享工作者线程截然不同。它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。
WorkerGlobalScope
在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有 window 的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self
关键字暴露出来。如下是 WorkerGlobalScope 属性和方法:
self
上可用的属性是 window 对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本。
属性/方法 | 类型 | 描述 |
---|---|---|
self | 属性 | 指向自身的引用,可以用于在 worker 脚本中访问 WorkerGlobalScope 对象。 |
location | 属性 | 返回 WorkerLocation 对象,表示工作者上下文的地址信息。 |
navigator | 属性 | 返回 WorkerNavigator 对象,提供关于工作者的代理、语言和其他上下文信息。 |
onerror | 属性 | 一个处理错误事件的事件处理程序,当 worker 中发生错误时触发。 |
onlanguagechange | 属性 | 当用户的语言设置改变时触发的事件处理程序。 |
onoffline | 属性 | 当 worker 从在线状态转为离线状态时触发的事件处理程序。 |
ononline | 属性 | 当 worker 从离线状态转为在线状态时触发的事件处理程序。 |
onrejectionhandled | 属性 | 当 worker 处理了一个先前被拒绝的 promise 时触发的事件处理程序。 |
onunhandledrejection | 属性 | 当 worker 中的一个 promise 被拒绝且没有处理程序时触发的事件处理程序。 |
importScripts() | 方法 | 用于动态导入脚本到 worker 中。 |
setTimeout() | 方法 | 在指定的时间后执行一个函数或指定的代码片段。 |
clearTimeout() | 方法 | 清除由 setTimeout() 设置的定时器。 |
setInterval() | 方法 | 在指定的时间间隔内重复执行一个函数或代码片段。 |
clearInterval() | 方法 | 清除由 setInterval() 设置的定时器。 |
fetch() | 方法 | 用于发起 HTTP 请求并返回包含响应的 promise。 |
atob() | 方法 | 解码一个 Base64 编码的字符串。 |
btoa() | 方法 | 创建一个字符串的 Base64 编码版本。 |
addEventListener() | 方法 | 添加事件监听器来处理特定事件类型。 |
removeEventListener() | 方法 | 移除使用 addEventListener() 添加的事件监听器。 |
dispatchEvent() | 方法 | 触发指定事件,调用绑定到事件的事件处理程序。 |
WorkerGlobalScope 的子类
实际上并不是所有地方都实现了 WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这 继承自 WorkerGlobalScope。
- 专用工作者线程使用 DedicatedWorkerGlobalScope。
- 共享工作者线程使用 SharedWorkerGlobalScope。
- 服务工作者线程使用 ServiceWorkerGlobalScope。
专用工作者线程
专用工作者线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。
基本概念
可以把专用工作者线程称为后台脚本(background script)。JavaScript 线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。该脚本也可以再请求其他脚本, 但一个线程总是从一个脚本源开始。
创建专用工作者线程
创建专用工作者线程最常见的方式是加载 JavaScript 文件。把文件路径提供给 Worker 构造函数, 然后构造函数再在后台异步加载脚本并实例化工作者线程。传给构造函数的文件路径可以是多种形式。
// worker.js 与 main.js 同一目录下
const worker = new Worker('worker.js');
工作者线程本身存在于一个独立的 JavaScript 环境中,因此 main.js 必须以 Worker 对象为代理实现与工作者线程通信。
工作者线程安全限制
工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误。
// 尝试基于 https://example.com/worker.js 创建工作者线程
const sameOriginWorker = new Worker('./worker.js');
// 尝试基于 https://untrusted.com/worker.js 创建工作者线程
const remoteOriginWorker = new Worker('https://untrusted.com/worker.js');
// Error: Uncaught DOMException: Failed to construct 'Worker':
// Script at https://untrusted.com/main.js cannot be accessed
// from origin https://example.com
基于加载脚本创建的工作者线程不受文档的内容安全策略限制,因为工作者线程在与父文档不同的上下文中运行。不过,如果工作者线程加载的脚本带有全局唯一标识符(与加载自一个二进制大文件一样),就会受父文档内容安全策略的限制。
使用 Worker 对象
Worker()
构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。
要管理好使用 Worker()
创建的每个 Worker 对象。在终止工作者线程之前,它不会被垃圾回收,也不能通过编程方式恢复对之前 Worker 对象的引用。
Worker 对象支持下列事件处理程序属性:
属性/方法 | 说明 |
---|---|
onerror | 在工作者线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序。可以通过 worker.onerror 或 worker.addEventListener('error', handler) 的形式处理。 |
onmessage | 当工作者线程向主线程发送消息时触发。可以通过 worker.onmessage 或 worker.addEventListener('message', handler) 处理。 |
onmessageerror | 当工作者线程收到无法反序列化的消息时触发。可以通过 worker.onmessageerror 或 worker.addEventListener('messageerror', handler) 处理。 |
postMessage() | 用于向工作者线程发送信息,主线程与工作者线程之间的异步通信机制。 |
terminate() | 立即终止工作者线程,不提供清理资源的机会。脚本会立刻停止执行。 |
DedicatedWorkerGlobalScope
在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例。因为这继承自 WorkerGlobalScope,所以包含它的所有属性和方法。工作者线程可以通过 self
关键字访问该全局作用域。
顶级脚本和工作者线程中的 console
对象都将写入浏览器控制台,这对于调试非常有用。因为工作者线程具有不可忽略的启动延迟,所以即使 Worker 对象存在,工作者线程的日志也会在主线程的日志之后打印出来。
DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法。
属性/方法 | 说明 |
---|---|
name | 可以提供给 Worker 构造函数的一个可选的字符串标识符。 |
postMessage() | 与 worker.postMessage() 对应的方法,用于从工作者线程内部向父上下文发送消息。 |
close() | 与 worker.terminate() 对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。 |
importScripts() | 用于向工作者线程中导入任意数量的脚本。 |
专用工作者线程与隐式 MessagePorts
专用工作者线程的 Worker 对象和 DedicatedWorkerGlobalScope 与 MessagePorts 有一些相同接口处理程和方法:onmessage
、onmessageerror
、close()
和 postMessage()
。这不是偶然的,因为专用工作者线程隐式使用了 MessagePorts 在两个上下文之间通信。
父上下文中的 Worker 对象和 DedicatedWorkerGlobalScope 实际上融合了 MessagePort,并在自己的接口中分别暴露了相应的处理程序和方法。换句话说,消息还是通过 MessagePort 发送,只是没有直接使用 MessagePort 而已。
也有不一致的地方,比如 start()
和 close()
约定。专用工作者线程会自动发送排队的消息,因此 start()
也就没有必要了。另外,close()
在专用工作者线程的上下文中没有意义,因为这样关闭 MessagePort 会使工作者线程孤立。因此,在工作者线程内部调用 close()
(或在外部调用 terminate()
) 不仅会关闭 MessagePort,也会终止线程。