引入异步 JavaScript
在本文中,我们将解释什么是异步编程、为什么需要它,并简要讨论 JavaScript 历史上实现异步函数的一些方法。
¥In this article, we'll explain what asynchronous programming is, why we need it, and briefly discuss some of the ways asynchronous functions have historically been implemented in JavaScript.
先决条件: | 对 JavaScript 基础知识有一定的了解,包括函数和事件处理程序。 |
---|---|
目标: | 熟悉什么是异步 JavaScript、它与同步 JavaScript 有何不同,以及为什么我们需要它。 |
异步编程是一种技术,使你的程序能够启动可能长时间运行的任务,并且在该任务运行时仍然能够响应其他事件,而不必等到该任务完成。该任务完成后,你的程序就会显示结果。
¥Asynchronous programming is a technique that enables your program to start a potentially long-running task and still be able to be responsive to other events while that task runs, rather than having to wait until that task has finished. Once that task has finished, your program is presented with the result.
浏览器提供的许多功能,尤其是最有趣的功能,可能需要很长时间,因此是异步的。例如:
¥Many functions provided by browsers, especially the most interesting ones, can potentially take a long time, and therefore, are asynchronous. For example:
- 使用
fetch()
发出 HTTP 请求 - 使用
getUserMedia()
访问用户的摄像头或麦克风 - 要求用户使用
showOpenFilePicker()
选择文件
因此,即使你可能不需要经常实现自己的异步函数,但你很可能需要正确使用它们。
¥So even though you may not have to implement your own asynchronous functions very often, you are very likely to need to use them correctly.
在本文中,我们将首先研究长时间运行的同步函数的问题,这使得异步编程成为必要。
¥In this article, we'll start by looking at the problem with long-running synchronous functions, which make asynchronous programming a necessity.
同步编程
¥Synchronous programming
考虑以下代码:
¥Consider the following code:
const name = "Miriam";
const greeting = `Hello, my name is ${name}!`;
console.log(greeting);
// "Hello, my name is Miriam!"
这段代码:
¥This code:
- 声明一个名为
name
的字符串。 - 声明另一个名为
greeting
的字符串,它使用name
。 - 将问候语输出到 JavaScript 控制台。
我们应该注意到,浏览器实际上按照我们编写的顺序一次一行地执行程序。在每一点,浏览器都会等待该行完成其工作,然后再继续下一行。它必须这样做,因为每一行都取决于前面几行所做的工作。
¥We should note here that the browser effectively steps through the program one line at a time, in the order we wrote it. At each point, the browser waits for the line to finish its work before going on to the next line. It has to do this because each line depends on the work done in the preceding lines.
这使得它成为一个同步程序。即使我们调用单独的函数,它仍然是同步的,如下所示:
¥That makes this a synchronous program. It would still be synchronous even if we called a separate function, like this:
function makeGreeting(name) {
return `Hello, my name is ${name}!`;
}
const name = "Miriam";
const greeting = makeGreeting(name);
console.log(greeting);
// "Hello, my name is Miriam!"
这里,makeGreeting()
是一个同步函数,因为调用者必须等待该函数完成其工作并返回一个值,然后调用者才能继续。
¥Here, makeGreeting()
is a synchronous function because the caller has to wait for the function to finish its work and return a value before the caller can continue.
长时间运行的同步函数
¥A long-running synchronous function
如果同步函数耗时较长怎么办?
¥What if the synchronous function takes a long time?
下面的程序使用一种非常低效的算法,当用户单击 "生成素数" 按钮时生成多个大素数。用户指定的素数数量越多,操作所需的时间就越长。
¥The program below uses a very inefficient algorithm to generate multiple large prime numbers when a user clicks the "Generate primes" button. The higher the number of primes a user specifies, the longer the operation will take.
<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>
<div id="output"></div>
const MAX_PRIME = 1000000;
function isPrime(n) {
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) {
return false;
}
}
return n > 1;
}
const random = (max) => Math.floor(Math.random() * max);
function generatePrimes(quota) {
const primes = [];
while (primes.length < quota) {
const candidate = random(MAX_PRIME);
if (isPrime(candidate)) {
primes.push(candidate);
}
}
return primes;
}
const quota = document.querySelector("#quota");
const output = document.querySelector("#output");
document.querySelector("#generate").addEventListener("click", () => {
const primes = generatePrimes(quota.value);
output.textContent = `Finished generating ${quota.value} primes!`;
});
document.querySelector("#reload").addEventListener("click", () => {
document.location.reload();
});
尝试单击 "生成素数"。根据计算机的速度,程序可能需要几秒钟才能显示 "完成的!" 消息。
¥Try clicking "Generate primes". Depending on how fast your computer is, it will probably take a few seconds before the program displays the "Finished!" message.
长时间运行的同步函数的问题
¥The trouble with long-running synchronous functions
下一个示例与上一个示例类似,只是我们添加了一个文本框供你输入。这次,单击 "生成素数",然后尝试在紧随其后的文本框中键入内容。
¥The next example is just like the last one, except we added a text box for you to type in. This time, click "Generate primes", and try typing in the text box immediately after.
你会发现,当我们的 generatePrimes()
函数运行时,我们的程序完全没有响应:你无法输入任何内容、单击任何内容或执行任何其他操作。
¥You'll find that while our generatePrimes()
function is running, our program is completely unresponsive: you can't type anything, click anything, or do anything else.
<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>
textarea {
display: block;
margin: 1rem 0;
}
const MAX_PRIME = 1000000;
function isPrime(n) {
for (let i = 2; i <= Math.sqrt(n); i++) {
if (n % i === 0) {
return false;
}
}
return n > 1;
}
const random = (max) => Math.floor(Math.random() * max);
function generatePrimes(quota) {
const primes = [];
while (primes.length < quota) {
const candidate = random(MAX_PRIME);
if (isPrime(candidate)) {
primes.push(candidate);
}
}
return primes;
}
const quota = document.querySelector("#quota");
const output = document.querySelector("#output");
document.querySelector("#generate").addEventListener("click", () => {
const primes = generatePrimes(quota.value);
output.textContent = `Finished generating ${quota.value} primes!`;
});
document.querySelector("#reload").addEventListener("click", () => {
document.location.reload();
});
原因是这个 JavaScript 程序是单线程的。线程是程序遵循的指令序列。因为程序由单个线程组成,所以它一次只能做一件事:因此,如果它正在等待我们长时间运行的同步调用返回,它就无法执行任何其他操作。
¥The reason for this is that this JavaScript 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.
我们需要的是我们的程序能够:
¥What we need is a way for our program to:
- 通过调用函数来启动长时间运行的操作。
- 让该函数开始操作并立即返回,以便我们的程序仍然可以响应其他事件。
- 让函数以不阻塞主线程的方式执行操作,例如启动一个新线程。
- 当操作最终完成时,通知我们操作结果。
这正是异步函数使我们能够做的事情。本模块的其余部分解释了如何在 JavaScript 中实现它们。
¥That's precisely what asynchronous functions enable us to do. The rest of this module explains how they are implemented in JavaScript.
事件处理程序
¥Event handlers
我们刚刚看到的异步函数的描述可能会让你想起事件处理程序,如果确实如此,那么你是对的。事件处理程序实际上是异步编程的一种形式:你提供一个函数(事件处理程序),该函数不会立即调用,而是在事件发生时调用。如果 "事件" 是 "异步操作已完成",则该事件可用于通知调用者异步函数调用的结果。
¥The description we just saw of asynchronous functions might remind you of event handlers, and if it does, you'd be right. Event handlers are really a form of asynchronous programming: you provide a function (the event handler) that will be called, not right away, but whenever the event happens. If "the event" is "the asynchronous operation has completed", then that event could be used to notify the caller about the result of an asynchronous function call.
一些早期的异步 API 正是以这种方式使用事件。XMLHttpRequest
API 使你能够使用 JavaScript 向远程服务器发出 HTTP 请求。由于这可能需要很长时间,因此它是一个异步 API,并且你可以通过将事件监听器附加到 XMLHttpRequest
对象来获取有关请求的进度和最终完成的通知。
¥Some early asynchronous APIs used events in just this way. The XMLHttpRequest
API enables you to make HTTP requests to a remote server using JavaScript. Since this can take a long time, it's an asynchronous API, and you get notified about the progress and eventual completion of a request by attaching event listeners to the XMLHttpRequest
object.
以下示例展示了这一点的实际效果。按 "点击开始请求" 发送请求。我们创建一个新的 XMLHttpRequest
并监听它的 loadend
事件。处理程序记录 "完成的!" 消息以及状态代码。
¥The following example shows this in action. Press "Click to start request" to send a request. We create a new XMLHttpRequest
and listen for its loadend
event. The handler logs a "Finished!" message along with the status code.
添加事件监听器后,我们发送请求。请注意,在此之后,我们可以记录 "开始 XHR 请求":也就是说,我们的程序可以在请求进行时继续运行,并且当请求完成时我们的事件处理程序将被调用。
¥After adding the event listener we send the request. Note that after this, we can log "Started XHR request": that is, our program can continue to run while the request is going on, and our event handler will be called when the request is complete.
<button id="xhr">Click to start request</button>
<button id="reload">Reload</button>
<pre readonly class="event-log"></pre>
pre {
display: block;
margin: 1rem 0;
}
const log = document.querySelector(".event-log");
document.querySelector("#xhr").addEventListener("click", () => {
log.textContent = "";
const xhr = new XMLHttpRequest();
xhr.addEventListener("loadend", () => {
log.textContent = `${log.textContent}Finished with status: ${xhr.status}`;
});
xhr.open(
"GET",
"https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json",
);
xhr.send();
log.textContent = `${log.textContent}Started XHR request\n`;
});
document.querySelector("#reload").addEventListener("click", () => {
log.textContent = "";
document.location.reload();
});
这与 我们在前面的模块中遇到的事件处理程序 类似,只不过事件不是用户操作(例如用户单击按钮),而是某个对象状态的更改。
¥This is just like the event handlers we've encountered in a previous module, except that instead of the event being a user action, such as the user clicking a button, the event is a change in the state of some object.
回调
¥Callbacks
事件处理程序是一种特定类型的回调。回调只是传递给另一个函数的函数,期望回调将在适当的时间被调用。正如我们刚才看到的,回调曾经是 JavaScript 中实现异步函数的主要方式。
¥An event handler is a particular type of callback. A callback is just a function that's passed into another function, with the expectation that the callback will be called at the appropriate time. As we just saw, callbacks used to be the main way asynchronous functions were implemented in JavaScript.
但是,当回调本身必须调用接受回调的函数时,基于回调的代码可能会变得难以理解。如果你需要执行一些分解为一系列异步函数的操作,这是一种常见的情况。例如,请考虑以下情况:
¥However, callback-based code can get hard to understand when the callback itself has to call functions that accept a callback. This is a common situation if you need to perform some operation that breaks down into a series of asynchronous functions. For example, consider the following:
function doStep1(init) {
return init + 1;
}
function doStep2(init) {
return init + 2;
}
function doStep3(init) {
return init + 3;
}
function doOperation() {
let result = 0;
result = doStep1(result);
result = doStep2(result);
result = doStep3(result);
console.log(`result: ${result}`);
}
doOperation();
这里我们有一个操作分为三个步骤,每个步骤都取决于最后一步。在我们的示例中,第一步向输入添加 1,第二步添加 2,第三步添加 3。从输入 0 开始,最终结果为 6 (0 + 1 + 2 + 3)。作为一个同步程序,这是非常简单的。但是如果我们使用回调来实现这些步骤呢?
¥Here we have a single operation that's split into three steps, where each step depends on the last step. In our example, the first step adds 1 to the input, the second adds 2, and the third adds 3. Starting with an input of 0, the end result is 6 (0 + 1 + 2 + 3). As a synchronous program, this is very straightforward. But what if we implemented the steps using callbacks?
function doStep1(init, callback) {
const result = init + 1;
callback(result);
}
function doStep2(init, callback) {
const result = init + 2;
callback(result);
}
function doStep3(init, callback) {
const result = init + 3;
callback(result);
}
function doOperation() {
doStep1(0, (result1) => {
doStep2(result1, (result2) => {
doStep3(result2, (result3) => {
console.log(`result: ${result3}`);
});
});
});
}
doOperation();
因为我们必须在回调内部调用回调,所以我们得到了一个深度嵌套的 doOperation()
函数,这使得阅读和调试变得更加困难。有时将其称为 "回调地狱" 或 "厄运金字塔"(因为凹痕的侧面看起来像金字塔)。
¥Because we have to call callbacks inside callbacks, we get a deeply nested doOperation()
function, which is much harder to read and debug. This is sometimes called "callback hell" or the "pyramid of doom" (because the indentation looks like a pyramid on its side).
当我们像这样嵌套回调时,处理错误也会变得非常困难:通常,你必须在 "pyramid" 的每个级别处理错误,而不是仅在顶层处理一次错误。
¥When we nest callbacks like this, it can also get very hard to handle errors: often you have to handle errors at each level of the "pyramid", instead of having error handling only once at the top level.
由于这些原因,大多数现代异步 API 不使用回调。相反,JavaScript 异步编程的基础是 Promise
,这也是下一篇文章的主题。
¥For these reasons, most modern asynchronous APIs don't use callbacks. Instead, the foundation of asynchronous programming in JavaScript is the Promise
, and that's the subject of the next article.