使用模块 worker 进行 Web 线程处理

现在,使用 Web Worker 中的 JavaScript 模块可以更轻松地将繁重工作转移到后台线程。

JavaScript 是单线程的,这意味着它一次只能执行一项操作。这是 非常直观,适用于网络上的很多情况,但在我们需要 处理繁重的任务,如数据处理、解析、计算或分析。随着 在 Web 上分发复杂应用后,对多线程的 处理。

在网络平台上,线程处理和并行处理的主要基元是 Web Workers API。 工作器是操作系统之上的轻量级抽象概念 一个线程来公开传递消息传递 API 实现线程间通信在执行代价高昂的计算或 在大型数据集上运行,这使得主线程在执行 在一个或多个后台线程上执行开销大的操作。

以下是一个典型的工作器使用示例,其中工作器脚本监听来自主实例的消息 会话,并通过回发自己的消息来进行响应:

page.js:

const worker = new Worker('worker.js');
worker.addEventListener('message', e => {
  console.log(e.data);
});
worker.postMessage('hello');

worker.js:

addEventListener('message', e => {
  if (e.data === 'hello') {
    postMessage('world');
  }
});

在大多数浏览器中,Web Worker API 已经使用了十多年。虽然 这也意味着工作者拥有极佳的浏览器支持和经过精心优化,这也意味着他们 JavaScript 模块之前的版本。由于设计 worker 时没有模块系统,因此 API 用于将代码加载到 Worker,并且编写脚本与使用同步脚本 2009 年的常见加载方法。

历史记录:传统版工作器

Worker 构造函数采用经典的 脚本网址,即 相对于文档网址它会立即返回对新工作器实例的引用, 它公开了一个消息传递接口以及一个 terminate() 方法,该方法会立即停止并停止响应 会销毁 Worker。

const worker = new Worker('worker.js');

Web Worker 中提供了 importScripts() 函数,用于加载其他代码,但 暂停 worker 的执行,以便获取和评估每个脚本。它还负责执行脚本 像传统的 <script> 标记一样,这意味着一个脚本中的变量可以 被另一个中的变量覆盖

worker.js:

importScripts('greet.js');
// ^ could block for seconds
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

// global to the whole worker
function sayHello() {
  return 'world';
}

因此,Web Worker 向来对网络架构的 应用。开发者必须打造巧妙的工具和解决方法, 使用 Web Worker,而无需放弃现代开发实践。例如, webpack 将一个小型模块加载器实现嵌入到使用 importScripts() 的生成代码中 但会将模块封装在函数中,以避免变量冲突,并模拟 依赖项导入和导出

输入模块 worker

为 Web Worker 提供了一种新模式,具有 JavaScript 的工效学设计和性能优势 模块(称为模块工作器)在 Chrome 80 中内置。通过 Worker 构造函数现在接受新的 {type:"module"} 选项,该选项会更改脚本加载和 以便与 <script type="module"> 匹配。

const worker = new Worker('worker.js', {
  type: 'module'
});

由于模块工作器是标准的 JavaScript 模块,因此它们可以使用 import 和 export 语句。如 对于所有 JavaScript 模块,依赖项仅在给定上下文(主线程、 worker 等),而所有后续导入都会引用已执行的模块实例。加载 JavaScript 模块的执行和执行也由浏览器进行优化。模块的依赖项可以是 在模块执行之前加载,这样可以将整个模块树 并行运行。模块加载还会缓存已解析的代码,这意味着主 在 Worker 中,只需要解析一次。

迁移到 JavaScript 模块还允许使用动态 import 用于延迟加载代码而不阻止 worker。动态导入比使用 importScripts() 加载依赖项要明确得多, 因为系统返回的是导入的模块的导出,而不是依赖于全局变量。

worker.js:

import { sayHello } from './greet.js';
addEventListener('message', e => {
  postMessage(sayHello());
});

greet.js:

import greetings from './data.js';
export function sayHello() {
  return greetings.hello;
}

为了确保出色的性能,旧版 importScripts() 方法在模块中不可用 worker。将 worker 切换为使用 JavaScript 模块意味着所有代码都以严格的 模式。另一个 值得注意的变化是,JavaScript 模块的顶级作用域内 this 的值改为 undefined,而在传统版 worker 中,该值是 worker 的全局范围。幸运的是, 始终是一个 self 全局变量,提供对全局范围的引用。该服务已在以下国家/地区推出: 所有类型的 worker(包括 Service Worker)以及 DOM 中。

使用 modulepreload 预加载工作器

模块工作器的一项实质性性能改进是能够预加载 worker 及其依赖项。使用模块工作器时,脚本会作为标准加载和执行 JavaScript 模块,这意味着可以使用 modulepreload 预加载甚至预先解析这些模块:

<!-- preloads worker.js and its dependencies: -->
<link rel="modulepreload" href="worker.js">

<script>
  addEventListener('load', () => {
    // our worker code is likely already parsed and ready to execute!
    const worker = new Worker('worker.js', { type: 'module' });
  });
</script>

主线程和模块 worker 也可以使用预加载的模块。这对于 在两种上下文中都导入的模块,或者在无法提前知道的情况下 某个模块将用在主线程上还是在 worker 中使用。

以前,用于预加载 Web Worker 脚本的选项有限, 必须可靠。传统工作器有自己的“工作器”可预加载的资源类型,但无 浏览器实现了 <link rel="preload" as="worker">。因此,主要技术 那就是使用 <link rel="prefetch">,它完全依赖于 HTTP 缓存中当与正确的缓存标头结合使用时,这使得这成为可能 避免工作器实例化必须等待下载工作器脚本。但是,与 modulepreload,此方法不支持预加载依赖项或准备解析。

共享工作器呢?

共享工作器 已更新为从 Chrome 83 开始支持 JavaScript 模块。与专用工作器一样, 使用 {type:"module"} 选项构建共享 worker 现在会将 worker 脚本作为 模块,而不是传统脚本:

const worker = new SharedWorker('/worker.js', {
  type: 'module'
});

在支持 JavaScript 模块之前,SharedWorker() 构造函数只需要 网址和可选的 name 参数。这仍可用于传统共享工作器;不过 创建模块共享 worker 需要使用新的 options 参数。可用的 选项 与专用 Worker 的相同,包括取代 name 选项, 上一个 name 参数。

Service Worker 呢?

Service Worker 规范 已更新,以支持接受 将 JavaScript 模块作为入口点,并使用与模块工作器相同的 {type:"module"} 选项, 但这项更改尚未在浏览器中实施。完成后,就可以 使用以下代码通过 JavaScript 模块实例化 Service Worker:

navigator.serviceWorker.register('/sw.js', {
  type: 'module'
});

由于该规范已更新,因此浏览器将开始实现新行为。 这需要一些时间,因为引入 JavaScript 会带来一些额外的复杂问题 Service Worker。Service Worker 注册需要比较导入的脚本 确定是否触发更新,并且需要针对 JavaScript 模块实现这一点 当用于 Service Worker 时。此外,Service Worker 需要能够绕过 对脚本进行缓存 检查更新。

其他资源和补充阅读材料