介绍工作线程

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

¥In this final article in our "Asynchronous JavaScript" module, we'll introduce workers, which enable you to run some tasks in a separate thread of execution.

先决条件: 对 JavaScript 基础知识有一定的了解,包括事件处理。
目标: 了解如何使用网络工作者。

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

¥In the first article of this module, we saw what happens when you have a long-running synchronous task in your program — the whole window becomes totally unresponsive. Fundamentally, the reason for this is that the program is single-threaded. A thread is a sequence of instructions that a program follows. Because the program consists of a single thread, it can only do one thing at a time: so if it is waiting for our long-running synchronous call to return, it can't do anything else.

Workers 使你能够在不同的线程中运行某些任务,因此你可以启动任务,然后继续其他处理(例如处理用户操作)。

¥Workers give you the ability to run some tasks in a different thread, so you can start the task, then continue with other processing (such as handling user actions).

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

¥One concern from all this is that if multiple threads can have access to the same shared data, it's possible for them to change it independently and unexpectedly (with respect to each other). This can cause bugs that are hard to find.

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

¥To avoid these problems on the web, your main code and your worker code never get direct access to each other's variables, and can only truly "share" data in very specific cases. Workers and the main code run in completely separate worlds, and only interact by sending each other messages. In particular, this means that workers can't access the DOM (the window, document, page elements, and so on).

工人分为三种不同类型:

¥There are three different sorts of workers:

  • 敬业的工人
  • 共享工人
  • 服务工作进程

在本文中,我们将介绍第一种工作人员的示例,然后简要讨论其他两种工作人员。

¥In this article, we'll walk through an example of the first sort of worker, then briefly discuss the other two.

使用网络工作者

¥Using web workers

还记得在第一篇文章中,我们有一个计算素数的页面吗?我们将使用工作人员来运行素数计算,以便我们的页面保持对用户操作的响应。

¥Remember in the first article, where we had a page that calculated prime numbers? We're going to use a worker to run the prime-number calculation, so our page stays responsive to user actions.

同步素数生成器

¥The synchronous prime generator

让我们首先再看一下前面示例中的 JavaScript:

¥Let's first take another look at the JavaScript in our previous example:

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() 之后,程序变得完全没有响应。

¥In this program, after we call generatePrimes(), the program becomes totally unresponsive.

有工人的黄金一代

¥Prime generation with a worker

对于此示例,首先在 https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start 处制作文件的本地副本。该目录下有四个文件:

¥For this example, start by making a local copy of the files at https://github.com/mdn/learning-area/blob/main/javascript/asynchronous/workers/start. There are four files in this directory:

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

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

¥The "index.html" file and the "style.css" files are already complete:

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",将工作代码添加到 "generate.js"。

¥The "main.js" and "generate.js" files are empty. We're going to add the main code to "main.js", and the worker code to "generate.js".

首先,我们可以看到工作代码与主代码保存在单独的脚本中。我们还可以看到,查看上面的 "index.html",<script> 元素中仅包含主要代码。

¥So first, we can see that the worker code is kept in a separate script from the main code. We can also see, looking at "index.html" above, that only the main code is included in a <script> element.

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

¥Now copy the following code into "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() 构造函数创建工作线程。我们向它传递一个指向工作脚本的 URL。一旦创建了 worker,就会执行 worker 脚本。
  • 接下来,与同步版本一样,我们将 click 事件处理程序添加到 "生成素数" 按钮。但现在,我们不再调用 generatePrimes() 函数,而是使用 worker.postMessage() 向工作线程发送消息。该消息可以接受一个参数,在本例中,我们传递一个包含两个属性的 JSON 对象:
    • command:一个字符串,标识我们希望工作人员做的事情(以防我们的工作人员可以做不止一件事)
    • quota:要生成的素数数量。
  • 接下来,我们向工作线程添加一个 message 事件处理程序。这样工作人员就可以告诉我们它何时完成,并向我们传递任何结果数据。我们的处理程序从消息的 data 属性中获取数据,并将其写入输出元素(数据与 quota 完全相同,因此这有点毫无意义,但它显示了原理)。
  • 最后,我们为 "重新加载" 按钮实现 click 事件处理程序。这与同步版本完全相同。

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

¥Now for the worker code. Copy the following code into "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);
}

请记住,它会在主脚本创建工作线程后立即运行。

¥Remember that this runs as soon as the main script creates the worker.

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

¥The first thing the worker does is start listening for messages from the main script. It does this using addEventListener(), which is a global function in a worker. Inside the message event handler, the data property of the event contains a copy of the argument passed from the main script. If the main script passed the generate command, we call generatePrimes(), passing in the quota value from the message event.

generatePrimes() 函数就像同步版本一样,只是我们在完成后不返回值,而是向主脚本发送一条消息。为此,我们使用 postMessage() 函数,它与 addEventListener() 一样是工作线程中的全局函数。正如我们已经看到的,主脚本正在监听此消息,并在收到消息时更新 DOM。

¥The generatePrimes() function is just like the synchronous version, except instead of returning a value, we send a message to the main script when we are done. We use the postMessage() function for this, which like addEventListener() is a global function in a worker. As we already saw, the main script is listening for this message and will update the DOM when the message is received.

注意:To run this site, you'll have to run a local web server, because file:// URLs are not allowed to load workers.请参阅我们的 设置本地测试服务器 指南。完成此操作后,你应该能够单击 "生成素数" 并使主页保持响应状态。

¥Note: To run this site, you'll have to run a local web server, because file:// URLs are not allowed to load workers. See our guide to setting up a local testing server. With that done, you should be able to click "Generate primes" and have your main page stay responsive.

如果你在创建或运行示例时遇到任何问题,你可以查看 成品版 并尝试 live

¥If you have any problems creating or running the example, you can review the finished version and try it live.

其他工种

¥Other types of workers

我们刚刚创建的工人就是所谓的敬业工人。这意味着它由单个脚本实例使用。

¥The worker we just created was what's called a dedicated worker. This means it's used by a single script instance.

不过,还有其他类型的工人:

¥There are other types of workers, though:

结论

¥Conclusion

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

¥In this article we've introduced web workers, which enable a web application to offload tasks to a separate thread. The main thread and the worker don't directly share any variables, but communicate by sending messages, which are received by the other side as message events.

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

¥Workers can be an effective way to keep the main application responsive, although they can't access all the APIs that the main application can, and in particular can't access the DOM.

也可以看看