## 1 引言

本周精读的文章是 [speedy-introduction-to-web-workers](https://auth0.com/blog/speedy-introduction-to-web-workers/)，是一篇 Web Workers 快速入门的文章，借精读这篇文章的机会，谈谈对 Web Workers 的理解与运用。

## 2 概述

> 就像分工，你只负责编码，而你的朋友负责设计，那你就可以专心把自己的事情做好，而且更快速的完成任务。

本文通过一个比方，描述了 Web Workers 的两大特征：

1. 高效。
2. 并行。

因为浏览器是单线程的，任何大量耗时的 JS 任务都会卡住界面，使浏览器无法响应任何操作，这样的用户体验非常糟糕。Web Workers 可以将耗时任务拆解出去，降低主线程的压力，避免主线程无响应。

> 但 CPU 资源是有限的，Web Workers 并不能增加总体运行效率，算上通信的损耗，整体计算效率会有一定的下降。

### 创建 Web Workers

```javascript
const worker = new Worker("../src/worker.js");
```

上述代码中，`worker` 就是一个 Web Workers 实例，执行的代码是 `../src/worker.js` 路径下的文件。

### 收发消息

Web Workers 用来执行异步脚本，只要掌握了它与主线程通信的方式，就可以在指定时机运行异步脚本，并在运行完时将结果传递给主线程。

#### 主线程接收发 Web Workers 消息

```javascript
const worker = new Worker("../src/worker.js");

worker.onmessage = e => {};

worker.postMessage("Marco!");
```

每个 `worker` 实例通过 `onmessage` 接收消息，通过 `postMessage` 发送消息。

#### Web Workers 收发主线程消息

```javascript
self.onmessage = e => {};

self.postMessage("Marco!");
```

和主线程代码类似，在 Web Workers 代码中，也是 `onmessage` 接收消息，这个消息来自主线程或者其它 Workers。也可以通过 `postMessage` 发送消息。

#### 销毁 Web Workers

```javascript
worker.terminate();
```

文章内容就这么多，是不是有写太简单了呢！笔者结合自己的使用经验，再补充一些知识。

## 3 精读

### 对象转移（Transferable Objects）

对象转移就是将对象引用零成本转交给 Web Workers 的上下文，而不需要进行结构拷贝。

这里要解释的是，**主线程与 Web Workers 之间的通信，并不是对象引用的传递，而是序列化/反序列化的过程**，当对象非常庞大时，序列化和反序列化都会消耗大量计算资源，降低运行速度。

![](https://img.alicdn.com/tfs/TB17Y7BjgHqK1RjSZFgXXa7JXXa-577-318.png)

上面的图充分证明了，大对象传递，使用对象转移各项指标都优于结构拷贝。

对象转移使用方式很简单，给 `postMessage` 增加一个参数，把对象引用传过去即可：

```javascript
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
```

浏览器兼容性也不错：Currently Chrome 17+, Firefox, Opera, Safari, IE10+。更具体内容，可以看 [Transferable Objects: Lightning Fast!](https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast)。

> 需要注意的是，对象引用转移后，原先上下文就无法访问此对象了，需要在 Web Workers 再次将对象还原到主线程上下文后，主线程才能正常访问被转交的对象。

### 如何不用 JS 文件创建 Web Workers

Web Workers 优势这么大，但用起来需要在同域下创建一个 JS 文件实在不方便，尤其在前后端分离做的比较彻底的团队，前端团队能控制的仅仅是一个 JS 文件。那么下面给出几个不用 JS 文件，就创建 Web Workers 的方法：

#### webpack 插件 - worker-loader

[worker-loader](https://github.com/webpack-contrib/worker-loader) 是一个 webpack 插件，可以将一个普通 JS 文件的全部依赖提取后打包并替换调用处，以 Blob 形式内联在源码中。

```javascript
import Worker from "worker-loader!./file.worker.js";

const worker = new Worker();
```

上述代码的魔术在于，转化成下面的方式执行：

```javascript
const blob = new Blob([codeFromFileWorker], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
```

#### Blob URL

第二种方式由第一种方式自然带出：如果不想用 webpack 插件，那自己通过 Blob 的方式创建也可以：

```javascript
const code = `
  importScripts('https://xxx.com/xxx.js');
  self.onmessage = e => {};
`;

const blob = new Blob([code], { type: "application/javascript" });
const worker = new Worker(URL.createObjectURL(blob));
```

看上去代码更轻量一些，不过问题是当遇到复杂依赖时，如果不能把依赖都转化为脚本通过 `importScripts` 方式引用，就无法访问到主线程环境中的包。如果真的遇到了这个问题，可以用第一种 webpack 插件的方式解决，这个插件会自动把文件所有依赖都打包进源码。

### 管理 postMessage 队列

为什么 postMessage 会形成队列，为什么要管理它？

首先在 Web Workers 架构设计上就必须做成队列，因为调用 `postMessage` 时，对应的 Web Workers 不一定完成了初始化，所以浏览器底层必须管理一个队列，在 Web Workers 初始化完毕时，依次消费，这样才能确保任何时候发出的 `postMessage` 都能被 Web Workers 接收到。

其次，为什么要手动维护这个队列，原因可能取决于如下几点：

- 业务原因，前面的 `postMessage` 还没来得及消费，就不要发送新的消息，或者丢弃新的消息，这时候需要通过双向通信拿到 Web Workers 的执行结果回执，手动控制队列。
- 性能原因，一般 Web Workers 都会被用来执行耗时的同步运算，如果运算时间比较长，那短期塞入多个消息队列是没有意义的。

![](https://img.alicdn.com/tfs/TB1QzMIjbPpK1RjSZFFXXa5PpXa-1828-982.png)

如上图所示，对于每次用户输入都要进行的 SQL Parser 很耗时，及时放在 Web Workers 也可能导致将 Workers 撑爆到无响应，这是不仅要使用多 Workers 缓冲池，还要对待执行队列进行过滤，因为用户永远只关心最后一次输入的 Parser 结果。

由于 Web Workers 运算被卡住时，除了销毁 Worker 没有别的办法，而销毁 Worker 的成本比较高，不能对每一个用户输入都销毁并新建 Web Workers，所以利用 Workers 缓冲池，当缓冲池满了，新的消费队列又进来的时候，可以销毁全部 Workers 缓冲池，换一批新缓冲池重新消费用户输入。

## 4 总结

Web Workers 是拆解异步计算的好帮手，vscode 网页版也通过 Web Workers 异步完成代码提示和高亮，笔者有对比过，发现 Web Workers 性能提升非常明显。

管理好你的 Web Workers 消息队列，谨防同步计算让 Web Workers 失去响应，建立一个智能的消息队列，根据业务需求设计一个最好的队列消费模型吧！

## 5 更多讨论

> 讨论地址是：[精读《谈谈 Web Workers》 · Issue #108 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/108)

**如果你想参与讨论，请[点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
