JavaScript 异步编程与回调

编程语言中的异步性

计算机生来就是异步的。

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

在当前的消费级计算机中,每个程序都会运行一个特定的时间片,然后停止执行,让另一个程序继续执行。这个过程以极快的速度循环进行,以至于我们无法察觉。我们以为计算机能同时运行多个程序,但这其实是一种错觉(多处理器机器除外)。

程序在内部使用*中断*,这是一种向处理器发出的信号,以获得系统的关注。

我们现在不深入探讨其内部原理,但请记住,程序是异步的是正常的,它们会暂停执行直到需要关注,从而允许计算机在此期间执行其他任务。当一个程序在等待网络响应时,它不能一直占用处理器直到请求完成。

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

JavaScript

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

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

const  = 1;
const  = 2;
const  =  * ;
.();
doSomething();

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

答案在于它的环境。浏览器通过提供一组可以处理这类功能的 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)。