介绍 Worker
在“异步 JavaScript”模块的最后一篇文章中,我们将介绍 worker,它使你能够在单独的线程中运行某些任务。
预备知识 | 对本模块前面课程中介绍的 JavaScript 基础知识和异步概念有扎实的理解。 |
---|---|
学习成果 |
|
在本模块的第一篇文章中,我们看到了当程序中有一个长时间运行的同步任务时会发生什么——整个窗口变得完全无响应。从根本上说,这是因为程序是 单线程 的。 线程 是程序遵循的一系列指令。由于程序由单个线程组成,它一次只能做一件事:因此,如果它正在等待我们长时间运行的同步调用返回,它就不能做任何其他事情。
Worker 让你能够在一个不同的线程中运行一些任务,这样你就可以启动任务,然后继续进行其他处理(例如处理用户操作)。
所有这一切的一个问题是,如果多个线程可以访问相同的共享数据,它们可能会独立地、意外地(相对于彼此)更改它。这可能会导致难以发现的错误。
为了避免 Web 上的这些问题,你的主代码和 Worker 代码永远无法直接访问彼此的变量,并且只能在非常特定的情况下真正“共享”数据。Worker 和主代码运行在完全独立的世界中,并且只能通过相互发送消息进行交互。特别是,这意味着 Worker 无法访问 DOM(窗口、文档、页面元素等)。
有三种不同类型的 Worker:
- 专用 Worker
- 共享 Worker
- 服务 Worker
在本文中,我们将通过一个第一种 Worker 的示例,然后简要讨论其他两种。
使用 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()
来完成此操作,该函数是 Worker 中的一个全局函数。在 message
事件处理程序内部,事件的 data
属性包含从主脚本传递的参数的副本。如果主脚本传递了 generate
命令,我们会调用 generatePrimes()
,并传入来自消息事件的 quota
值。
generatePrimes()
函数与同步版本类似,不同之处在于,它不是返回一个值,而是在完成时向主脚本发送一条消息。我们为此使用 postMessage()
函数,该函数与 addEventListener()
一样是 Worker 中的一个全局函数。正如我们已经看到的,主脚本正在监听此消息,并在收到消息时更新 DOM。
注意:要运行此网站,您必须运行本地 Web 服务器,因为不允许 file:// URL 加载 Worker。请参阅如何设置本地测试服务器?以了解操作方法。完成此操作后,您应该能够单击“生成质数”并使您的主页面保持响应。
其他类型的 Worker
我们刚刚创建的 Worker 称为 专用 Worker。这意味着它由单个脚本实例使用。
不过,还有其他类型的 Worker:
- 共享 Worker 可以由在不同窗口中运行的几个不同脚本共享。
- Service Worker 充当代理服务器,缓存资源,以便 Web 应用程序在用户离线时也能工作。它们是渐进式 Web 应用程序的关键组件。
总结
在本文中,我们介绍了 Web Worker,它使 Web 应用程序能够将任务分流到单独的线程。主线程和 Worker 不直接共享任何变量,而是通过发送消息进行通信,这些消息由另一方作为 message
事件接收。
Worker 是一种保持主应用程序响应的有效方式,尽管它们无法访问主应用程序可以访问的所有 API,尤其无法访问 DOM。