异步
JS 中有三种重要的 异步 编程语言特性:
ES6 的 Promise 对象;
ES2017 的 async/await 关键字;
ES2018 的 for-await 异步迭代。
使用回调的异步编程
在最基本层面上,JS 异步是使用回调实现的。回调即作为其他函数的参数的函数。这个其他函数会在满足某个情况时调用这个函数。
定时器
可以用 setTimeout()
来在指定时间后执行一段代码,这段代码就是以回调的传入。该函数第一个参数为回调函数,第二个为时间间隔,单位为毫秒,剩余参数会传入回调函数中,返回值为一个定时器编号:
setTimeout(() => { /* ... */ }, 1000) // 1 秒后执行传入的函数
或者用 setInterval()
和 clearInterval()
来周期性执行回调。
使用定时器函数时,其 this 指向可能和预期不同,具体可见 关于"this"的问题 。
事件
客户端 JS 编程几乎全是事件驱动的,它会等待用户的动作事件来执行相应的代码。事件驱动的 JS 程序在特定上下文中为特定类型的事件注册回调函数,而浏览器在指定事件发生时调用这些函数。
可以通过事件目标的 addEventListener()
方法来注册事件,事件目标可以是 DOM 元素、document
、window
等。比如设定一个按钮的点击事件:
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)
}
}
参考
EventTarget.addEventListener() - MDN
Promises/A+ - promisesaplus.com