JavaScript Web Worker 和非主线程任务入门

2025-06-10

JavaScript Web Worker 和非主线程任务入门

浏览器中的 JavaScript 在设计上是单线程的,这意味着我们所有的 JavaScript 代码将共享同一个调用堆栈。乍一看,这似乎有点难以置信;我们一直使用Promises执行并发操作。然而,这种并发性(以及setTimeoutsetInterval其他并发性)是使用事件循环实现的。

通常来说,这已经足够了,尤其是对于那些主要使用 HTTP 和服务器获取并显示数据,或接受输入并持久化数据的应用来说。然而,随着客户端应用变得越来越复杂,越来越像应用程序,我们倾向于在浏览器中运行越来越多的 JavaScript,这会给我们的单线程(或“主线程”)带来压力。幸运的是,我们有Web Workers通过在后台线程中运行 JavaScript 代码来帮助我们减轻主线程的压力!

什么是 Web Worker?

根据 MDN 的说法,Web Worker是一种让 Web 内容在后台线程中运行脚本的简单方法。它们不要与 Service Worker 混淆,后者负责代理应用程序的网络请求。Web Worker 的价值在于它支持并行性,使您的应用程序能够同时运行多个 JavaScript 执行上下文。

使用 Web Workers 时需要考虑几个重要的限制:

  1. Web Workers 在完全独立的 JavaScript 环境中执行,不与主线程共享内存,而是通过消息进行通信
  2. Workers 的全局作用域与主 JS 线程不同:没有window对象,因此没有 DOM,localStorage等等
  3. 你的 worker 的实际 JS 代码必须位于单独的文件中(稍后会详细介绍)

尽管 Web Workers 的使用频率不高,但它已经存在很长时间了,并且得到了所有主流浏览器的支持,甚至可以追溯到 IE 10(来源

Web Workers API 浏览器支持图表

关于并发与并行的简要说明

虽然并发和并行乍一看似乎是指代同一概念的两个术语,但它们并非一回事。简而言之,并发是指在多个任务上同时执行,且执行顺序不一;想象一下,你的应用程序会fetch多次调用,并在每个 Promise 解析后执行某个任务,但执行顺序不一定正确,也不会阻塞其余代码。而并行是指在多个 CPU 或多个 CPU 核心上同时执行多个任务。想了解更多关于并发的知识,请查看这篇很棒的 StackOverflow 文章,其中有几种不同的解释!

基本示例

好了,讲解得够多了,我们来看一些代码!要创建一个新的Worker实例,必须使用构造函数,如下所示:

// main.js
const worker = new Worker('path/to/worker.js');
Enter fullscreen mode Exit fullscreen mode

如上所述,此路径实际上必须指向与主 bundle 不同的 JavaScript 文件。因此,您可能需要配置 bundler 或构建链以处理 Web Workers。如果您使用的是Parcel,Web Workers 是开箱即用的!因此,在本文的其余部分,我们将使用 Parcel。使用 Parcel 时,您可以通过传递Worker 实际源代码的相对路径来构造 Worker 实例,如下所示:

// main.js
const worker = new Worker('./worker.js');
Enter fullscreen mode Exit fullscreen mode

注意:如果您在 CodeSandbox 中使用 Parcel,则此功能目前不受支持。您可以克隆一个类似这样的Parcel 样板文件,或者自己创建一个,然后在本地进行实验。

这太棒了,因为现在我们可以在 Worker 代码中使用 NPM 模块和各种 ESNext 特性了,Parcel 会帮我们处理打包成独立 bundle 的任务!🎉

可惜的是,worker.js它还不存在……让我们来创建它吧。以下是我们的 Web Worker 的最小样板:

// worker.js
function handleMessage(event) {
  self.postMessage(`Hello, ${event.data}!`);
}

self.addEventListener('message', handleMessage);
Enter fullscreen mode Exit fullscreen mode

注意,我们self在这里使用 而不是window。现在,让我们回到主脚本,通过向 Worker 发送消息并处理响应来测试它:

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

function handleMessage(event) {
  console.log(event.data);
}

worker.addEventListener('message', handleMessage);

worker.postMessage('Mehdi');
// Hello, Mehdi!
Enter fullscreen mode Exit fullscreen mode

这应该够用了!这是使用 Web Worker 的最小设置。不过,“hello world”应用并不太占用 CPU……我们来看一个更具体的例子,看看 Web Worker 何时能派上用场。

弹力球示例

为了说明 Web Workers 的实用性,让我们使用一个执行效率极低的递归斐波那契数列计算器,如下所示:

// fib.js
function fib(position) {
  if (position === 0) return 0;
  if (position === 1) return 1;
  return fib(position - 1) + fib(position - 2);
}

export default fib;
Enter fullscreen mode Exit fullscreen mode

在我们的计算器中间,我们想要一个弹力球,就像这样:

斐波那契计算器旁边的弹力球

弹跳动画循环进行requestAnimationFrame,这意味着浏览器会每隔约 16 毫秒尝试绘制一次球。如果主线程 JavaScript 的执行时间超过这个时间,就会出现丢帧和视觉卡顿。在充满交互和动画的实际应用中,这种情况会非常明显!让我们尝试计算位置处的斐波那契数40,看看会发生什么:

弹力球在计算时冻结

我们的动画在代码运行时至少会卡顿 1.2 秒!这不足为奇,因为递归fib函数总共被调用了 331160281 次,而调用堆栈却从未被清除。另外需要注意的是,这完全取决于用户的 CPU。本次测试是在 2017 款 MacBook Pro 上进行的。将 CPU 节流设置为 6 倍速后,时间飙升至 12 秒以上。

让我们用 Web Worker 来解决这个问题。不过,与其postMessage在应用程序代码中处理各种调用和事件监听,不如围绕 Web Worker 实现一个更友好的基于 Promise 的接口。

首先,让我们创建我们的工作者,我们称之为fib.worker.js

// fib.worker.js
import fib from './fib';

function handleMessage(event) {
  const result = fib(event);
  self.postMessage(result);
};

self.addEventListener('message', handleMessage);
Enter fullscreen mode Exit fullscreen mode

这和我们之前的 Worker 示例类似,只是多了对函数的调用fib。现在,让我们创建一个asyncFib函数,它最终将接受一个位置参数,并返回一个 Promise,该 Promise 将解析为该位置的斐波那契数列。

// asyncFib.js
function asyncFib(pos) {
  // We want a function that returns a Promise that resolves to the answer
  return new Promise((resolve, reject) => {
    // Instantiate the worker
    const worker = new Worker('./fib.worker.js');

    // ... do the work and eventually resolve
  })
}

export default asyncFib;
Enter fullscreen mode Exit fullscreen mode

我们知道需要处理来自 Worker 的消息才能获取fib函数的返回值,所以让我们创建一个message事件处理程序来捕获消息,并使用其中包含的数据来解析 Promise。我们还将在worker.terminate()处理程序内部调用,这将销毁 Worker 实例以防止内存泄漏:

// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    // Create our message event handler
    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    // Mount message event handler
    worker.addEventListener('message', handleMessage);
  })
}
Enter fullscreen mode Exit fullscreen mode

我们也来处理一下error事件。如果 Worker 遇到错误,我们希望用 error 事件来拒绝 Promise。因为这是任务的另一个退出场景,所以我们也想worker.terminate()在这里调用:

// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    // Create our error event handler
    function handleError(err) {
      worker.terminate();
      reject(err);
    }

    worker.addEventListener('message', handleMessage);
    // Mount our error event listener
    worker.addEventListener('error', handleError);
  })
}
Enter fullscreen mode Exit fullscreen mode

最后,让我们postMessage使用pos参数的值来调用以启动一切!

// asyncFib.js
function asyncFib(pos) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./fib.worker.js');

    function handleMessage(e) {
      worker.terminate();
      resolve(e.data);
    }

    function handleError(err) {
      worker.terminate();
      reject(err);
    }

    worker.addEventListener('message', handleMessage);
    worker.addEventListener('error', handleError);

    // Post the message to the worker
    worker.postMessage(pos);
  })
}
Enter fullscreen mode Exit fullscreen mode

这样就大功告成了。最后还有一件事要做:检查一下它是否正常工作。让我们看看我们的应用程序在40使用新asyncFib函数计算位置处的斐波那契数时是什么样子的:

弹力球在计算时继续弹跳

好多了!我们成功解锁了主线程,让小球继续弹跳,同时还创建了一个美观的界面来操作我们的asyncFib函数。

如果您好奇,请试用示例应用程序查看 GitHub 上的代码

总结

Web Worker API 是一款功能强大且尚未得到充分利用的工具,它有望成为未来前端开发的重要组成部分。如今,许多低端移动设备占据了 Web 用户的大部分份额,这些设备 CPU 速度较慢,但​​拥有多个核心,因此采用非主线程架构将大有裨益。我喜欢分享 Web Worker 的内容,撰写文章/发表演讲,如果您感兴趣,请在 Twitter 上关注我。

这里还有一些其他有用的资源,可以激发您的创造力:

感谢阅读!

链接地址:https://dev.to/mvasigh/getting-started-with-javascript-web-workers-and-off-main-thread-tasks-4029
PREV
我看到了网络的未来,那就是以太坊
NEXT
我如何克服拖延症