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