介绍 Worker

在“异步 JavaScript”模块的最后一篇文章中,我们将介绍 worker,它使你能够在单独的线程中运行某些任务。

预备知识 对本模块前面课程中介绍的 JavaScript 基础知识和异步概念有扎实的理解。
学习成果
  • 如何使用专用 Web Worker,以及原因。
  • 了解其他类型 Web Worker 的用途,例如共享 Worker 和服务 Worker。

在本模块的第一篇文章中,我们看到了当程序中有一个长时间运行的同步任务时会发生什么——整个窗口变得完全无响应。从根本上说,这是因为程序是 单线程 的。 线程 是程序遵循的一系列指令。由于程序由单个线程组成,它一次只能做一件事:因此,如果它正在等待我们长时间运行的同步调用返回,它就不能做任何其他事情。

Worker 让你能够在一个不同的线程中运行一些任务,这样你就可以启动任务,然后继续进行其他处理(例如处理用户操作)。

所有这一切的一个问题是,如果多个线程可以访问相同的共享数据,它们可能会独立地、意外地(相对于彼此)更改它。这可能会导致难以发现的错误。

为了避免 Web 上的这些问题,你的主代码和 Worker 代码永远无法直接访问彼此的变量,并且只能在非常特定的情况下真正“共享”数据。Worker 和主代码运行在完全独立的世界中,并且只能通过相互发送消息进行交互。特别是,这意味着 Worker 无法访问 DOM(窗口、文档、页面元素等)。

有三种不同类型的 Worker:

  • 专用 Worker
  • 共享 Worker
  • 服务 Worker

在本文中,我们将通过一个第一种 Worker 的示例,然后简要讨论其他两种。

使用 Web Worker

还记得第一篇文章中,我们有一个计算质数的页面吗?我们将使用 Worker 来运行质数计算,这样我们的页面就能对用户操作保持响应。

同步质数生成器

让我们首先回顾一下我们上一个示例中的 JavaScript 代码:

js
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  return primes;
}

document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  const primes = generatePrimes(quota);
  document.querySelector("#output").textContent =
    `Finished generating ${quota} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});

在此程序中,调用 generatePrimes() 后,程序会完全无响应。

使用 Worker 生成质数

对于此示例,首先在 https://github.com/mdn/learning-area/tree/main/javascript/asynchronous/workers/start 创建文件的本地副本。此目录中有四个文件:

  • index.html
  • style.css
  • main.js
  • generate.js

"index.html" 和 "style.css" 文件已完成。

html
<!doctype html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Prime numbers</title>
    <script src="main.js" defer></script>
    <link href="style.css" rel="stylesheet" />
  </head>

  <body>
    <label for="quota">Number of primes:</label>
    <input type="text" id="quota" name="quota" value="1000000" />

    <button id="generate">Generate primes</button>
    <button id="reload">Reload</button>

    <textarea id="user-input" rows="5" cols="62">
Try typing in here immediately after pressing "Generate primes"
    </textarea>

    <div id="output"></div>
  </body>
</html>
css
textarea {
  display: block;
  margin: 1rem 0;
}

"main.js" 和 "generate.js" 文件是空的。我们将把主代码添加到 "main.js",把 Worker 代码添加到 "generate.js"。

因此,首先我们可以看到 Worker 代码与主代码分开存放在一个单独的脚本中。我们还可以看到,从上面的 "index.html" 文件来看,只有主代码包含在 <script> 元素中。

现在将以下代码复制到 "main.js" 中:

js
// Create a new worker, giving it the code in "generate.js"
const worker = new Worker("./generate.js");

// When the user clicks "Generate primes", send a message to the worker.
// The message command is "generate", and the message also contains "quota",
// which is the number of primes to generate.
document.querySelector("#generate").addEventListener("click", () => {
  const quota = document.querySelector("#quota").value;
  worker.postMessage({
    command: "generate",
    quota,
  });
});

// When the worker sends a message back to the main thread,
// update the output box with a message for the user, including the number of
// primes that were generated, taken from the message data.
worker.addEventListener("message", (message) => {
  document.querySelector("#output").textContent =
    `Finished generating ${message.data} primes!`;
});

document.querySelector("#reload").addEventListener("click", () => {
  document.querySelector("#user-input").value =
    'Try typing in here immediately after pressing "Generate primes"';
  document.location.reload();
});
  • 首先,我们使用 Worker() 构造函数创建 Worker。我们向它传递一个指向 Worker 脚本的 URL。Worker 一旦创建,Worker 脚本就会立即执行。

  • 接下来,与同步版本一样,我们为“生成质数”按钮添加了一个 click 事件处理程序。但现在,我们不是调用 generatePrimes() 函数,而是使用 worker.postMessage() 向 Worker 发送消息。此消息可以带一个参数,在本例中,我们传递一个包含两个属性的 JSON 对象:

    • command:一个字符串,用于标识我们希望 Worker 执行的操作(以防我们的 Worker 可以执行多项操作)
    • quota:要生成的质数数量。
  • 接下来,我们向 Worker 添加一个 message 事件处理程序。这样 Worker 就可以告诉我们它何时完成,并向我们传递任何结果数据。我们的处理程序从消息的 data 属性中获取数据,并将其写入输出元素(数据与 quota 完全相同,所以这有点无意义,但它展示了原理)。

  • 最后,我们实现了“重新加载”按钮的 click 事件处理程序。这与同步版本完全相同。

现在是 Worker 代码。将以下代码复制到 "generate.js" 中:

js
// Listen for messages from the main thread.
// If the message command is "generate", call `generatePrimes()`
addEventListener("message", (message) => {
  if (message.data.command === "generate") {
    generatePrimes(message.data.quota);
  }
});

// Generate primes (very inefficiently)
function generatePrimes(quota) {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  // When we have finished, send a message to the main thread,
  // including the number of primes we generated.
  postMessage(primes.length);
}

请记住,这会在主脚本创建 Worker 后立即运行。

Worker 所做的第一件事是开始监听来自主脚本的消息。它使用 addEventListener() 来完成此操作,该函数是 Worker 中的一个全局函数。在 message 事件处理程序内部,事件的 data 属性包含从主脚本传递的参数的副本。如果主脚本传递了 generate 命令,我们会调用 generatePrimes(),并传入来自消息事件的 quota 值。

generatePrimes() 函数与同步版本类似,不同之处在于,它不是返回一个值,而是在完成时向主脚本发送一条消息。我们为此使用 postMessage() 函数,该函数与 addEventListener() 一样是 Worker 中的一个全局函数。正如我们已经看到的,主脚本正在监听此消息,并在收到消息时更新 DOM。

注意:要运行此网站,您必须运行本地 Web 服务器,因为不允许 file:// URL 加载 Worker。请参阅如何设置本地测试服务器?以了解操作方法。完成此操作后,您应该能够单击“生成质数”并使您的主页面保持响应。

如果您在创建或运行示例时遇到任何问题,可以查看完成版本在线试用

其他类型的 Worker

我们刚刚创建的 Worker 称为 专用 Worker。这意味着它由单个脚本实例使用。

不过,还有其他类型的 Worker:

总结

在本文中,我们介绍了 Web Worker,它使 Web 应用程序能够将任务分流到单独的线程。主线程和 Worker 不直接共享任何变量,而是通过发送消息进行通信,这些消息由另一方作为 message 事件接收。

Worker 是一种保持主应用程序响应的有效方式,尽管它们无法访问主应用程序可以访问的所有 API,尤其无法访问 DOM。

另见