介绍 Worker
在本模块的最后一篇文章“异步 JavaScript”中,我们将介绍*workers*,它使您能够在单独的线程中运行一些任务。
先决条件 | 对 JavaScript 基础知识(包括事件处理)有基本的了解。 |
---|---|
目标 | 了解如何使用 Web Workers。 |
在本模块的第一篇文章中,我们看到了在程序中运行长时间同步任务时会发生什么 - 整个窗口完全无响应。从根本上说,出现这种情况的原因是程序是*单线程*的。一个*线程*是程序执行的一系列指令。由于程序仅包含一个线程,它一次只能执行一项操作:因此,如果它正在等待我们的长时间同步调用返回,它将无法执行任何其他操作。
Workers 使您能够在不同的线程中运行一些任务,这样您就可以启动任务,然后继续进行其他处理(例如处理用户操作)。
从这一切中,我们有一个担忧,如果多个线程可以访问相同的共享数据,它们可能会独立且意外地(相对于彼此)更改数据。这会导致难以查找的错误。
为了避免在 Web 上出现这些问题,您的主代码和 worker 代码永远不会直接访问彼此的变量,并且只能在非常特定的情况下真正“共享”数据。Workers 和主代码在完全独立的环境中运行,并且只通过互相发送消息进行交互。特别是,这意味着 Workers 无法访问 DOM(窗口、文档、页面元素等)。
有三种不同类型的 Workers
- 专用 Workers
- 共享 Workers
- 服务 Workers
在本文中,我们将通过第一个类型 Workers 的示例进行说明,然后简要讨论另外两种类型。
使用 Web Worker
还记得我们在第一篇文章中,有一个页面计算素数吗?我们将使用 Worker 来运行素数计算,这样我们的页面就能保持对用户操作的响应。
同步素数生成器
让我们首先再次看一下我们之前示例中的 JavaScript 代码
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”文件已经完成
<!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>
textarea {
display: block;
margin: 1rem 0;
}
“main.js”和“generate.js”文件为空。我们将把主代码添加到“main.js”中,将 Worker 代码添加到“generate.js”中。
因此首先,我们可以看到 Worker 代码与主代码保存在不同的脚本中。我们还可以看到,从上面的“index.html”可以看出,只有主代码包含在<script>
元素中。
现在将以下代码复制到“main.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”中
// 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。
其他类型的 Worker
我们刚刚创建的 Worker 被称为专用 Worker。这意味着它被单个脚本实例使用。
但是还有其他类型的 Workers
- 共享 Workers可以被在不同窗口中运行的多个不同脚本共享。
- 服务 Workers充当代理服务器,缓存资源,以便 Web 应用程序可以在用户离线时工作。它们是渐进式 Web 应用程序的关键组件。
结论
在本文中,我们介绍了 Web Workers,它使 Web 应用程序能够将任务卸载到单独的线程。主线程和 Worker 不直接共享任何变量,而是通过发送消息进行通信,这些消息以message
事件的形式被另一方接收。
Workers 可以有效地保持主应用程序的响应,尽管它们无法访问主应用程序可以访问的所有 API,特别是无法访问 DOM。