JavaScript 异步编程和回调
¥JavaScript Asynchronous Programming and Callbacks
编程语言中的异步性
¥Asynchronicity in Programming Languages
计算机在设计上是异步的。
¥Computers are asynchronous by design.
异步意味着事情可以独立于主程序流发生。
¥Asynchronous means that things can happen independently of the main program flow.
在当前的消费类计算机中,每个程序都会运行特定的时间段,然后停止执行以让另一个程序继续执行。这个东西的运行速度非常快,以至于不可能注意到。我们认为我们的计算机同时运行许多程序,但这是一种错觉(多处理器机器除外)。
¥In the current consumer computers, every program runs for a specific time slot and then it stops its execution to let another program continue their execution. This thing runs in a cycle so fast that it's impossible to notice. We think our computers run many programs simultaneously, but this is an illusion (except on multiprocessor machines).
程序在内部使用中断,这是一种向处理器触发的信号,以引起系统的注意。
¥Programs internally use interrupts, a signal that's emitted to the processor to gain the attention of the system.
现在我们先不深入讨论这个内部细节,但请记住,程序异步并暂停执行直到需要注意,这很正常,同时允许计算机执行其他操作。当程序等待网络响应时,它不能停止处理器,直到请求完成。
¥Let's not go into the internals of this now, but just keep in mind that it's normal for programs to be asynchronous and halt their execution until they need attention, allowing the computer to execute other things in the meantime. When a program is waiting for a response from the network, it cannot halt the processor until the request finishes.
通常,编程语言是同步的,有些语言提供了一种在语言中或通过库管理异步性的方法。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 默认都是同步的。其中一些通过使用线程来处理异步操作,从而产生一个新进程。
¥Normally, programming languages are synchronous and some provide a way to manage asynchronicity in the language or through libraries. C, Java, C#, PHP, Go, Ruby, Swift, and Python are all synchronous by default. Some of them handle async operations by using threads, spawning a new process.
JavaScript
JavaScript 默认是同步的并且是单线程的。这意味着代码无法创建新线程并并行运行。
¥JavaScript is synchronous by default and is single threaded. This means that code cannot create new threads and run in parallel.
代码行按顺序执行,一个接一个,例如:
¥Lines of code are executed in series, one after another, for example:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();
但 JavaScript 诞生于浏览器内部,它的主要工作是响应用户操作,如 onClick
、onMouseOver
、onChange
、onSubmit
等等。如何使用同步编程模型做到这一点?
¥But JavaScript was born inside the browser, its main job, in the beginning, was to respond to user actions, like onClick
, onMouseOver
, onChange
, onSubmit
and so on. How could it do this with a synchronous programming model?
答案在其环境中。浏览器提供了一种方法,通过提供一组可以处理此类功能的 API。
¥The answer was in its environment. The browser provides a way to do it by providing a set of APIs that can handle this kind of functionality.
最近,Node.js 引入了一个非阻塞 I/O 环境,将这个概念扩展到文件访问、网络调用等。
¥More recently, Node.js introduced a non-blocking I/O environment to extend this concept to file access, network calls and so on.
回调
¥Callbacks
你无法知道用户何时会单击按钮。因此,你为 click 事件定义了一个事件处理程序。此事件处理程序接受一个函数,该函数将在触发事件时被调用:
¥You can't know when a user is going to click a button. So, you define an event handler for the click event. This event handler accepts a function, which will be called when the event is triggered:
document.getElementById('button').addEventListener('click', () => {
// item clicked
});
这就是所谓的回调。
¥This is the so-called callback.
回调是一个简单的函数,它作为值传递给另一个函数,并且仅在事件发生时执行。我们可以这样做,因为 JavaScript 具有一等函数,可以将其分配给变量并传递给其他函数(称为高阶函数)
¥A callback is a simple function that's passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions)
通常将所有客户端代码封装在 window
对象上的 load
事件监听器中,该监听器仅在页面准备就绪时运行回调函数:
¥It's common to wrap all your client code in a load
event listener on the window
object, which runs the callback function only when the page is ready:
window.addEventListener('load', () => {
// window loaded
// do what you want
});
回调无处不在,而不仅仅是在 DOM 事件中。
¥Callbacks are used everywhere, not just in DOM events.
一个常见的例子是使用定时器:
¥One common example is by using timers:
setTimeout(() => {
// runs after 2 seconds
}, 2000);
XHR 请求也接受回调,在此示例中,通过将函数分配给将在发生特定事件时调用的属性(在本例中,请求的状态发生变化):
¥XHR requests also accept a callback, in this example by assigning a function to a property that will be called when a particular event occurs (in this case, the state of the request changes):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();
处理回调中的错误
¥Handling errors in callbacks
如何使用回调处理错误?一种非常常见的策略是使用 Node.js 采用的策略:任何回调函数中的第一个参数都是错误对象:错误优先回调
¥How do you handle errors with callbacks? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks
如果没有错误,则对象为 null
。如果出现错误,它包含一些错误描述和其他信息。
¥If there is no error, the object is null
. If there is an error, it contains some description of the error and other information.
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// handle error
console.log(err);
return;
}
// no errors, process data
console.log(data);
});
回调的问题
¥The problem with callbacks
回调非常适合简单的情况!
¥Callbacks are great for simple cases!
但是每个回调都会增加一层嵌套,当你有大量回调时,代码很快就会变得复杂:
¥However every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});
这只是一个简单的 4 级代码,但我见过更多级别的嵌套,这并不好玩。
¥This is just a simple 4-levels code, but I've seen much more levels of nesting and it's not fun.
我们如何解决这个问题?
¥How do we solve this?
回调的替代方案
¥Alternatives to callbacks
从 ES6 开始,JavaScript 引入了几个功能,可帮助我们处理不涉及使用回调的异步代码:Promises(ES6)和 Async/Await(ES2017)。
¥Starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks: Promises (ES6) and Async/Await (ES2017).