字节笔记本
2026年2月22日
JavaScript 异步编程与回调函数完全指南
计算机从设计之初就是异步的。异步意味着事情可以独立于主程序流程发生。
在当前的消费级计算机中,每个程序都在特定的时间片内运行,然后停止执行,让另一个程序继续执行。这个循环运行得如此之快,以至于我们无法察觉。我们认为计算机同时运行多个程序,但这只是一种错觉(多处理器机器除外)。
程序内部使用中断,这是一种向处理器发出的信号,用于获得系统的注意。我们先不深入讨论内部原理,但请记住,程序是异步的,在需要关注之前会暂停执行,这样计算机就可以同时执行其他任务,这是正常的。当程序等待网络响应时,它不能暂停处理器直到请求完成。
通常,编程语言是同步的,有些提供在语言中或通过库管理异步性的方式。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 默认都是同步的。其中一些通过使用线程、生成新进程来处理异步操作。
JavaScript 的同步特性
JavaScript 默认是同步的,而且是单线程的。这意味着代码不能创建新线程并并行运行。
代码行是串行执行的,一个接一个,例如:
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();但 JavaScript 诞生于浏览器中,它最初的主要工作是响应用户操作,如 onClick、onMouseOver、onChange、onSubmit 等。它如何用同步编程模型做到这一点呢?
答案在于它的环境。浏览器提供了一种方式来实现这一点,通过提供一组可以处理这种功能的 API。
最近,Node.js 引入了一个非阻塞 I/O 环境,将这个概念扩展到文件访问、网络调用等方面。
回调函数
你无法知道用户何时会点击按钮。因此,你为点击事件定义一个事件处理程序。这个事件处理程序接受一个函数,当事件被触发时,该函数将被调用:
document.getElementById('button').addEventListener('click', () => {
// item clicked
});这就是所谓的回调。
回调是一个简单的函数,作为值传递给另一个函数,并且只会在事件发生时执行。我们可以这样做是因为 JavaScript 有一等函数,可以分配给变量并传递给其他函数(称为高阶函数)。
通常将所有客户端代码包装在 window 对象的 load 事件监听器中是很常见的,它只在页面准备好时运行回调函数:
window.addEventListener('load', () => {
// window loaded
// do what you want
});回调函数无处不在,不仅仅在 DOM 事件中。
一个常见的例子是使用定时器:
setTimeout(() => {
// runs after 2 seconds
}, 2000);XHR 请求也接受回调,在这个例子中,通过将函数分配给一个在特定事件发生时将被调用的属性(在这种情况下,请求的状态发生变化):
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log(xhr.responseText);
} else {
console.error('error');
}
}
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();在回调中处理错误
如何用回调处理错误?一种非常常见的策略是使用 Node.js 采用的方式:任何回调函数中的第一个参数是错误对象,即错误优先回调。
如果没有错误,对象是 null。如果有错误,它包含一些错误描述和其他信息。
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);
});回调的问题
回调对于简单情况来说很棒!
然而,每个回调都会增加一层嵌套,当你有很多回调时,代码很快就会变得复杂:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// your code here
});
}, 2000);
});
});这只是一个简单的 4 层代码,但我见过更多层的嵌套,这并不有趣。
我们如何解决这个问题?
回调的替代方案
从 ES6 开始,JavaScript 引入了几个帮助我们处理异步代码的特性,这些特性不涉及使用回调:Promises(ES6)和 Async/Await(ES2017)。