天天看点

浅谈Web Worker和Service worker

Javascript 由于它的单线程特性,任何“重量”操作都会阻塞主线程,让我们的应用变得卡顿、看起来没有响应。为了提升性能和体验,现代浏览器允许我们将一些工作交给 Worker,把原本的单线程应用变成多线程运行。

浏览器中提供了 3 种 Worker,分别是:

  1. Web worker—— 包含专用 worker及共享 worker
  2. Service worker
  3. Worklet—— 包含PaintWorklet、AudioWorklet、AnimationWorklet、LayoutWorklet。

Worklet 似乎是专为媒体之类的应用设计,且仍处于试验状态。因此,本篇内容将只探讨更通用的 Web worker 及 Service worker。

Web worker

Web worker 特别适用于后台跑脚本。现在的网页都可以注册多个 Worker,让不同的任务在各自独立的环境中完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SmWroyUz-1666585948825)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4d9439fa4d074d5f85b8389ea839360a~tplv-k3u1fbpfcp-zoom-1.image)]

(图片来自Workers Overview)

用Worker API 创建一个页面专属线程的方法如下:

如果同源的不同标签想共享一个线程,则可以用SharedWorker API:

这两种 Worker 的用法近似,但在页面和 Worker 的连接上,共享 Worker 是通过端口来通信的。因此在注册共享 Worker 的时候必须指定一个可用的端口。然后在主线程中可以如下发送和接收消息:

mySharedWorker.port.postMessage("哈喽Worker!");
mySharedWorker.port.onmessage = (e) => {
  console.log("收到消息:", e.data);
};
           

在共享 Worker 跑的脚本中可以如下处理消息:

onconnect = (e) => {
  const port = e.ports[0];
  port.onmessage = (e) => {
    port.postMessage('收到消息 "${e.data}"`);
  };
}
           

专属 Worker 的通信则比共享 Worker 的更直观简洁。在主线程中可以如下发送和接收消息:

myWorker.postMessage('哈喽Worker!');
myWorker.onMessage = (e) => {
  console.log('收到消息:', e.data);
};
           

在专属 Worker 中可以如下处理消息:

onmessage = (e) => {
  postMessage('收到消息:"${e.data}"`);
};
           

此外,你可能还注意到一个区别是专属 Worker 与页面绑定在一起,因此页面关闭的话专属 Worker 也会被终止。

Service worker

Service worker 相当于是浏览器在网页和服务器通信中插入的一个“中间层”。因此它可以操作服务器请求,但由于安全和隐私性的考虑,浏览器对可以操作的范围有非常大的限制。

浅谈Web Worker和Service worker

(图片来自Workers Overview)

不同于 Web worker,它有一个更复杂的生命周期:

  1. 在主线程中用

    navigator.serviceWorker.register()

    注册 Service worker,然后浏览器异步下载 worker 用的脚本;
  2. 如果这个 Service worker 是新注册的,安装下载脚本。否则就更新已存在的脚本;
  3. 如果在这个 Service worker 声明管辖的范围内没有其他旧 worker 正在控制客户端,激活 worker;
  4. 如果脚本已有新版本替代,弃置旧脚本。

Service worker 拥有一些列API 及事件来做如下的事情:

  • 缓存数据
  • 拦截请求
  • 管理浏览器通知

基于这些能力,我们可以做很多有意思的事情。容我待会再聊。

他们的限制

Web worker 和 Service worker 功能强大,但也有不少限制。其中包括:

无权访问 DOM、window 对象和其它一些可能涉及隐私的 API,如 localStorage。这带来的有点事 Worker 的运行环境与页面的运行环境各自独立,互不打扰。然而这也意味着如果你想让 Worker 帮忙干些 DOM 相关的费力活是很麻烦的。

主线程和 Worker 之间通信的信息是个副本。这个副本用了一个算法叫the structured clone algorithm。这算法有点类似于

JSON.stringify()

JSON.parse()

,有很多数据类型在克隆的过程中被抹掉。且不同于 JSON 的接口,这个算法会直接让不支持的数据 key 都消失,甚至有时候抛出异常。

仅提供对 ES 模块的有限支持。虽说现代浏览器已大部分支持直接运行 ES 模块,但在 Worker 里它还是试验中的状态,支持的浏览器也很少。

As of Mar 1, 2019, only Chrome 80+ supports this feature, while Firefox has an open feature request. No other browsers are known to have support for production usage of worker scripts written as modules.

在支持的浏览器中,你可以如下打开一个有 ES 模块支持的 Worker:

new Worker("worker.js", {
  type: "module",
});
           

但含有关键字

import

export

的脚本并不能很好地工作,甚至会报错终止 Worker 的运行。

还有一个槽点是在 Worker 里打开一个新 Worker 这个在文档中有所提及的功能几乎没法用。有些第三方库比如

esbuild-wasm

内置了打开 Worker 运行的逻辑,那由这些库提供的功能都无法在 Worker 中调用的话,组织代码起来比较费力,需要让主线程帮忙转发消息。

继续阅读