Skip to content

JavaScript 异步编程和回调函数

编程语言中的异步性

计算机的设计本就是异步的。

异步意味着事情可以独立于主程序流程发生。

在目前的消费级计算机中,每个程序运行一个特定时间段,然后停止执行,让另一个程序继续执行。这个过程循环运行得非常快,以至于我们无法察觉。我们认为我们的计算机同时运行许多程序,但这是一种错觉(多处理器机器除外)。

程序内部使用中断,这是一种发送给处理器的信号,以引起系统的注意。

现在我们先不讨论其内部机制,只需记住程序异步以及暂停执行直到需要关注是正常的,这允许计算机在此期间执行其他任务。当程序等待网络响应时,它不能暂停处理器直到请求完成。

通常,编程语言是同步的,有些语言提供了一种方法来管理语言本身或通过库来管理异步性。C、Java、C#、PHP、Go、Ruby、Swift 和 Python 默认都是同步的。其中一些通过使用线程、生成新进程来处理异步操作。

JavaScript

JavaScript 默认是同步的,并且是单线程的。这意味着代码无法创建新线程并并行运行。

代码行是串行执行的,一个接一个,例如:

js
const a = 1;
const b = 2;
const c = a * b;
console.log(c);
doSomething();

但是 JavaScript 最初诞生于浏览器中,它最初的主要工作是响应用户操作,例如 onClickonMouseOveronChangeonSubmit 等等。它如何使用同步编程模型做到这一点呢?

答案在于它的环境。浏览器提供了一种方法,通过提供一组可以处理此类功能的 API 来实现。

最近,Node.js 引入了一个非阻塞 I/O 环境,将此概念扩展到文件访问、网络调用等等。

回调函数

你无法知道用户何时会点击按钮。因此,你为点击事件定义了一个事件处理程序。此事件处理程序接受一个函数,该函数将在触发事件时被调用:

js
document.getElementById('button').addEventListener('click', () => {
  // 点击项目
});

这就是所谓的回调函数

回调函数是一个简单的函数,作为值传递给另一个函数,并且只有在事件发生时才会执行。我们可以做到这一点是因为 JavaScript 具有头等函数,可以将其分配给变量并传递给其他函数(称为高阶函数)。

通常将所有客户端代码包装在window对象的load事件监听器中,该监听器仅在页面准备就绪时运行回调函数:

js
window.addEventListener('load', () => {
  // 窗口加载完毕
  // 执行你想要的操作
});

回调函数被广泛使用,不仅仅是在 DOM 事件中。

一个常见的例子是使用计时器:

js
setTimeout(() => {
  // 2 秒后运行
}, 2000);

XHR 请求也接受回调函数,在此示例中,通过将函数分配给一个属性来实现,该属性将在特定事件发生时被调用(在本例中,请求的状态发生变化):

js
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();

处理回调中的错误

如何处理回调中的错误?Node.js 采用了一种非常常见的策略:任何回调函数的第一个参数都是错误对象:错误优先回调

如果没有错误,则该对象为 null。如果发生错误,则包含错误的一些描述和其他信息。

js
const fs = require('node:fs');
fs.readFile('/file.json', (err, data) => {
  if (err) {
    // 处理错误
    console.log(err);
    return;
  }
  // 没有错误,处理数据
  console.log(data);
});

回调的问题

回调在简单情况下非常棒!

但是,每个回调都会增加一层嵌套,当您有很多回调时,代码很快就会变得很复杂:

js
window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {
        // 你的代码在这里
      });
    }, 2000);
  });
});

这只是一个简单的 4 层代码,但我见过更多层的嵌套,这并不有趣。

我们如何解决这个问题?

回调函数的替代方案

从 ES6 开始,JavaScript 引入了一些特性来帮助我们处理异步代码,而无需使用回调函数:Promise(ES6)和 Async/Await(ES2017)。