原文链接:JavaScript Asynchronous Programming and Callbacks by Flavio Copes

默认状态下 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 引入了一种非块式输入输出环境将这种概念扩展到文件访问、网络调用等方面。

回调函数

你不可能预知用户什么时候会点击一个按钮,为了及时处理点击事件,你事先定义一个 事件处理函数。这个事件处理函数的参数之一是一个函数,这个函数将在事件发生时被触发。

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

这就是所谓的 回调函数

回调函数是一个可以以值的形式传递给另外一个函数的简单函数,它仅在事件发生时执行。我们能这样操作是因为 JavaScript 中存在一级函数,它们可以被赋值给变量并且能在其他函数(高阶函数)之间传递。

最常见的应用是将你全部的客户端代码封装到 window 对象的一个 load 事件监听器中,这个监听器仅在页面加载完毕后执行回调函数。

window.addEventListener('load', () => { });

回调函数用途广泛,并非局限于处理 DOM 事件。

一个常见例子是用于计时器中:

setTimeout(() => {

}, 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 对象作为回调函数的第一个参数。此即 错误优先的回调函数

fs.readFile('/file.json', (err, data) => {
  if (err !== null) {
    console.log(err)
    return
  }

  console.log(data)
})

使用回调函数的弊端

回调函数非常适合简单情况。

尽管如此,每一个回调函数对应一层嵌套。使用大量回调函数会极大地增加代码的复杂度。

window.addEventListener('load', () => {
  document.getElementById('button').addEventListener('click', () => {
    setTimeout(() => {
      items.forEach(item => {

      });
    }, 2000);
  });
});

这段简单代码已经包含四层嵌套,读懂它稍费点劲,但我见过比这多许多层嵌套的代码,修改、使用它们可不是闹着玩的。

怎么解决这个问题?

回调函数的替代方案

从 ES6 开始,JavaScript 推出了多个帮助我们编写异步代码的功能: