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 诞生于浏览器中,它最初的主要工作是响应用户操作,例如onClickonMouseOveronChangeonSubmit等等。 使用同步编程模型如何做到这一点?

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

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

回调

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

document.getElementById('button').addEventListener('click', () => {
  // item clicked
});

这就是所谓的回调

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

通常的做法是将所有客户端代码包装在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) {
    xhr.status === 200 ? console.log(xhr.responseText) : console.error('error');
  }
};
xhr.open('GET', 'https://yoursite.com');
xhr.send();

在回调中处理错误

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

如果没有错误,则该对象为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)。