介绍 Worker

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

先决条件 对 JavaScript 基础知识(包括事件处理)有基本的了解。
目标 了解如何使用 Web Workers。

在本模块的第一篇文章中,我们看到了在程序中运行长时间同步任务时会发生什么 - 整个窗口完全无响应。从根本上说,出现这种情况的原因是程序是*单线程*的。一个*线程*是程序执行的一系列指令。由于程序仅包含一个线程,它一次只能执行一项操作:因此,如果它正在等待我们的长时间同步调用返回,它将无法执行任何其他操作。

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

从这一切中,我们有一个担忧,如果多个线程可以访问相同的共享数据,它们可能会独立且意外地(相对于彼此)更改数据。这会导致难以查找的错误。

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

有三种不同类型的 Workers

  • 专用 Workers
  • 共享 Workers
  • 服务 Workers

在本文中,我们将通过第一个类型 Workers 的示例进行说明,然后简要讨论另外两种类型。

使用 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()来完成此操作,addEventListener()是 Worker 中的全局函数。在message事件处理程序中,事件的data属性包含从主脚本传递的参数的副本。如果主脚本传递了generate命令,我们将调用generatePrimes(),并将来自消息事件的quota值传递给它。

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

注意:要运行此站点,您必须运行本地 Web 服务器,因为不允许 file:// URL 加载 Workers。请参阅我们关于设置本地测试服务器的指南。完成此操作后,您应该能够单击“生成素数”,并且您的主页面将保持响应。

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

其他类型的 Worker

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

但是还有其他类型的 Workers

结论

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

Workers 可以有效地保持主应用程序的响应,尽管它们无法访问主应用程序可以访问的所有 API,特别是无法访问 DOM。

另请参阅