跳到主要内容

异步

JS 中有三种重要的 异步 编程语言特性:

  • ES6 的 Promise 对象;

  • ES2017 的 async/await 关键字;

  • ES2018 的 for-await 异步迭代。

使用回调的异步编程

在最基本层面上,JS 异步是使用回调实现的。回调即作为其他函数的参数的函数。这个其他函数会在满足某个情况时调用这个函数。

定时器

可以用 setTimeout() 来在指定时间后执行一段代码,这段代码就是以回调的传入。该函数第一个参数为回调函数,第二个为时间间隔,单位为毫秒,剩余参数会传入回调函数中,返回值为一个定时器编号:

setTimeout(() => { /* ... */ }, 1000)   // 1 秒后执行传入的函数

或者用 setInterval()clearInterval() 来周期性执行回调。

使用定时器函数时,其 this 指向可能和预期不同,具体可见 关于"this"的问题

事件

客户端 JS 编程几乎全是事件驱动的,它会等待用户的动作事件来执行相应的代码。事件驱动的 JS 程序在特定上下文中为特定类型的事件注册回调函数,而浏览器在指定事件发生时调用这些函数。

可以通过事件目标的 addEventListener() 方法来注册事件,事件目标可以是 DOM 元素、documentwindow 等。比如设定一个按钮的点击事件:

let button = document.querySeletor("#my-button")
button.addEventListener("click", () => { /* ... */ }) // 点击事件

网络事件

网络请求是 JS 中常见的异步操作。客户端 JS 可以用 XMLHttpRequest 类来发送 HTTP 请求并异步处理返回的响应。比如我们从服务器获取数据:

function getCurrentVersion(resolve, reject) {
let request = new XMLHttpRequest()
request.open("GET", "http://www.example.com/api/version")
request.send()

request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
} else {
reject(request.statusText)
}
}

request.onerror = request.ontimeout = e => {
reject(e.type)
}
}

和之前的 addEventListener() 不同,这里通过将回调函数赋值给对象的属性来注册事件监听。具体使用见 XMLHttpRequest - MDN

XMLHttpRequest 类和 XML 没有关系,且现在这个类很大程度上已经被 fetch() 取代。

Node 中的回调 & 事件

Node 的 JS 环境底层就是异步的,且 Node 定义了很多使用回调及事件的 API 。比如读取文件的默认 API 就是异步的:

const fs = require("fs")

fs.readFile("data.json", "utf-8", (error, text) => {
if (err) {
console.warn("Could not read file:", error)
} else {
console.log(text)
}
})

Node 的 HTTP 请求也是基于事件的:

const http = require("http")

function getText(url, resolve, reject) {
let request = http.get(url)
request.on("response", response => {
// 这个事件意味着收到了 HTTP 响应头
let httpStatus = response.statusCode

// 但此时并没有收到 HTTP 响应体
response.setEncoding("utf-8")
let body = ""
response.on("data", chunk => { body += chunk })

// 响应完成会调用这个事件处理
response.on("end", () => {
if (httpStatus === 200) {
resolve(body)
} else {
reject(httpStatus)
}
})
})
request.on("error", error => {
reject(error)
})
}

Promise

Promise 是个对象,表示异步操作的结果。这个结果可能是 兑现 或者 拒绝 。Promise 会在获取到异步结果后执行我们指定的回调函数。

在基于回调的异步编程中会存在回调多层嵌套的情形,导致代码难以阅读,同时也较难处理异常。而 Promise 则通过链式调用及标准化异常处理来解决这两个问题。

在 JS 中,Promise 对象有三种状态:

  • pending 初始状态,即还未确定 Promise 对象是否兑现或拒绝;

  • fulfilled 表示 Promise 对象得到兑现;

  • rejected 表示 Promise 对象得到拒绝。

创建 Promise

Promise 构造函数接收一个执行函数,该函数有两个类型为函数的参数:

const p = new Promise((resolve, reject) => {
// ...
})

在执行函数中,调用 resolve(value) 会使 Promise 对象变为 fulfilled 状态,且 value 为兑现结果;反之调用 reject(reason) 会变为 rejected 状态,reason 为拒绝原因。

或者使用 Promise.resolve(value) 来返回一个根据给定值解析后的 Promise 对象:

Promise.resolve(10)     // 类似于 `new Promise(resolve => resolve(10))`

如果 value 是个 Promise 对象,则返回这个对象;如果是个 thenable 对象,则会采用 then() 方法执行后的状态;否则将以 value 值作为兑现结果完成。此函数将类 Promise 对象的多层嵌套展平。

thenable 即带有 then() 方法的对象,它的参数类似 Promise 构造函数中执行函数的参数。

亦或者使用 Promise.reject(reason) 来返回一个已被拒绝的 Promise 对象,reason 为其拒绝理由:

Promise.reject(new Error('Fail!'))

使用 Promise

通过 Promise 对象的 then() 方法来传递其为 fulfilled 或 rejected 状态时所用的回调:

promise.then(value => { /* ... */ }, reason => { /* ... */ })

then() 方法类似事件注册,如果多次调用同一个 Promise 对象的 then() 方法,则会按调用顺序执行各自的回调。且传入 then() 方法的回调会被异步执行。

同时,then() 会再次返回一个 Promise 对象,其状态取决于传入的回调。如果回调返回了一个值(没有返回则视为 undefined ),则为兑现;如果回调中抛出了错误,则为拒绝;如果返回了一个 Promise 对象,则延用其状态。

实际中很少会给 then() 传入拒绝时回调,而是使用 catch() 方法,也是返回一个 Promise 对象:

promise
.then(value => { /* ... */ })
.catch(reason => { /* ... */ })

上面的例子展示了 Promise 的链式调用。与上上个例子的作用近似等价。一个好的 Promise 链式调用应当使用到 catch() 方法来处理错误。

在 ES2018 中,还定义了 finally() 方法,其用途类似 try-catch-finally 中的 finally 子句。该方法不会给回调传入任何参数,但依旧返回一个 Promise 对象:

promise
.then(value => { /* ... */ })
.catch(reason => { /* ... */ })
.finally(() => { /* ... */ })

要记住的是,Promise 链中每一环的回调执行取决上一环的新 Promise 对象的返回。

并行多个 Promise 对象

如果希望通过多个 Promise 对象来执行多个异步操作,可以用 Promise.all()Promise.allSettled()Promise.race() 函数。它们均接收一个可迭代对象(通常是数组),而区别在于它们返回的 Promise :

  • Promise.all() 中若存在一个被拒绝的 Promise ,则返回的 Promise 也为拒绝,否则会以一个兑现结果数组完成;

  • Promise.allSettled() 返回的 Promise 会在所有 Promise 全部落定后兑现,永远不会被拒绝。且兑现结果为一个数组,数组元素为每个 Promise 的状态:

    let promises = [Promise.resolve(10), Promise.reject(20)]

    Promise.allSettled(promises).then(results => {
    results[0] // => { status: "fulfilled", value: 10 }
    results[1] // => { status: "rejected", reason: 20 }
    })
  • Promise.race() 返回的 Promise 会根据第一个落定的 Promise 。

串行多个 Promise 对象

可以手动实现串行多个 Promise 对象:

function sequence(promises) {
let p = Promise.resolve()
for (const promise of promises) {
p = p.then(() => promise)
}
return p.then(() => "done")
}

async & await

ES2017 新增了 async 和 await 关键字,它们简化了 Promise 的使用,允许我们可以像在写同步代码一样去写异步代码。

await 表达式

await 关键字接收一个 Promise 对象并将其转为一个返回值或一个异常。

对于 await p ,该表达式会一直等到 p 落定,如果 p 兑现,表达式返回兑现结果,如果 p 拒绝,则抛出其异常。

let response = await fetch("/api/user/profile")
let profile = await response.json()

response.json() 也是返回一个 Promise 对象。

上面的例子虽然看起来像是同步代码,但实际上还是基于 Promise 的异步代码。

async 函数

因为任何使用 await 的代码都是异步的,所以有个重要的规则就是,只能在 async 关键字声明的函数内部使用 await 关键字:

async function getUserProfile() {
let response = await fetch("/api/user/profile")
let profile = await response.json()
return profile
}

将函数声明为 async 意味着函数的返回值就是个 Promise 对象。如果函数正常返回,则按返回值兑现;若抛出异常,则拒绝。因此,可以对 async 函数的调用使用 async :

async function doSomething() {
let profile = await getUserProfile()
// ...
}

箭头函数、类和对象字面量中的方法简写都可以用 async 关键字。

由于后一个 await 语句需要等待后一个 await 完成,如果需要同时执行多个 Promise ,则可以使用 Promise.all()

let [value1, value2] = await Promise.all([promise1, promise2])

理解 async 工作原理

一个 async 函数:

async function f(x) { /* ... */ }

可以想象为一个返回 Promise 的包装函数:

function f(x) {
return new Promise((resolve, reject) => {
try {
resolve((x => { /* ... */ })(x))
} catch(e) {
reject(e)
}
})
}

而 await 则可以想象成分割同步代码块的记号。

异步迭代

Promise 自身只适合单次运行的异步计算,不适合与重复性异步事件一起使用,比如 setInterval() 、浏览器中的 click 事件、Node 流中的 data 事件等。

ES2018 则为此提供了异步迭代器及 for-await 。

for-await 循环

await 应当在 async 函数中使用,for-await 也一样。

for-await 的循环体会先等待循环头中 Promise 的兑现,才去执行:

for await (const response of promises) {
// ...
}

这类似于:

for (const promise of promises) {
const response = await promise
// ...
}

异步迭代器

异步迭代器与常规迭代器非常类似,主要有两个区别:

  • 使用 Symbol.asyncIterator 代替 Symbol.Iterator

  • next() 方法应当返回一个 Promise 对象,其兑现结果为迭代结果。

异步生成器

通过 async 来生成一个异步生成器。下面是一个类似 setInterval() 的异步生成器:

// 返回一个指定时间兑现的 Promise
function sleep(interval) {
return new Promise(resolve => {
setTimeout(resolve, interval)
})
}

// 定义了一个会睡眠一段时间再生成迭代值的生成器
async function* clock(interval) {
for (let count = 0; count < Infinity; count += 1) {
await sleep(interval)
yield count
}
}

// 按时打印计数
async function printTick() {
for await (const tick of clock(1000)) {
console.log(tick)
}
}

参考

window.setTimeout - MDN

EventTarget.addEventListener() - MDN

XMLHttpRequest - MDN

Promise - MDN

Fetch API - MDN

使用 Promise - MDN

Promises/A+ - promisesaplus.com

JS Promise (Part 1 - Basics) - Venkat.R, Medium

async 和 await :让异步编程更简单 - MDN

async 函数 - MDN