JavaScript Promise - 异步函数 (4/6)
JavaScript 设计 promise 的目的是将其用作高级语言特性的底层工具。异步函数 (async
) 就是这样一个高级语言特性,它抽象了 promise,使我们可以用编写常规代码的自上而下的方式来编写 promise
代码,而不必担心如何追踪管理 promise 及其 handler。
如何定义异步函数
异步函数可以用于任何同步函数能用的地方。大多数情况下,在函数或方法定义前添加一个 async
关键字就可以使其变为异步。下面是定义异步函数/方法的几种常见例子:
// 异步函数
async function doSomething() {}
// 异步箭头函数
const doSomething = async () => {}
// 对象中的异步方法
const object = {
async doSomething() {}
}
// 类中的异步方法
class MyClass {
async doSomething() {}
}
async
关键字表示这个函数/方法是异步的。对于 JavaScript 引擎来说,提前知道一个函数是否是异步函数是很重要的,因为它的行为与同步函数不同。
异步函数的独特之处
异步函数有四个主要的特点:
- 返回值总是一个 promise
- 如果抛出错误,返回的就是一个失败的 promise
- 可以使用
await
表达式 - 可以使用
for-await-of
循环
总是返回 promise
这和前面讲过的 promise 的 then()
、finally()
等方法是一样的:返回值不是 promise 的也会被 Promise.resolve()
方法包装成 promise。因为 async
关键字是在 promise 上做了一层抽象,但不改变 promise 本身。
async function doSomething() { return 2 }
const p1 = doSomething()
console.log(p1 instanceof Promise) // true
console.log(typeof p1 === 'number') // false
p1.then(v => console.log(v)) // 2
如果把一个外部的 promise 传递给 return
,那么这个 promise 不会被直接返回,只是返回其拷贝:
const p = Promise.resolve(2)
async function doSomething() { return p }
const p1 = doSomething()
console.log(p === p1) // false
如果不指定返回值,那么返回值默认是一个 promise,求值这个 promise 会得到 undefined
:
async function doSomething() {}
const p = doSomething()
console.log(p instanceof Promise) // true
p.then(v => console.log(v)) // undefined
这里的重点是:无论你在异步函数中做了什么乱七八糟的操作,它都会返回一个 promise。
抛出的错误
上面说了,异步函数无论如何都会返回 promise。抛出的错误也不例外,只不过返回的是一个失败的 promise。这里的重点是:我们不能用 try-catch
来捕获异步函数抛出的错误。
async function throwError() { throw new Error("Error") }
try {
throwError()
console.log("Didn't catch error")
} catch (e) {
// 这里永远不会执行
console.log("Never caught error")
}
因为异步函数返回的是 promise,我们只能用 promise 的方式来处理错误:
throwError().catch(reason => console.error(reason.message)) // Error
为了确保异步函数总是返回 promise,让我们有一个一致的方式来处理返回值,JavaScript 引擎也挺不容易的。
await
表达式
await
表达式是为了让使用 promise 更简单。使用 promise 时如果我们想要获得最终值,那么需要在 then()
等方法中调用 resolve 或 reject handler,await
的工作即隐式帮我们调用 handler。所以通过 await
我们直接就可以获得最终值,这使我们可以写出类似同步函数自上而下的代码,也使我们可以使用 try-catch
捕获错误。
// 不使用 await
function getJSONData(url) {
return fetch(url)
.then((response) => {
if (response.ok) {
return response.json()
} else {
throw new Error(`${response.statusText}`)
}
})
.catch((reason) => console.error(reason.message))
}
// 使用 await
async function getJSONData(url) {
try {
const response = await fetch(url)
if (response.ok) {
return await response.json()
} else {
throw new Error(`${response.statusText}`)
}
} catch (e) {
console.error(e.message)
}
}
await
还可以和非 promise 的值一起使用,同样,这些值通过 Promise.resolve()
被隐式包装为 promise。
async function doSomething() {
return await 2
}
doSomething().then(v => console.log(v)) // 2
await
表达式作用在单个 promise 上,然而上一篇讲述的 Promise.all()
等四个方法作用在多个 promise 上,且返回单个 promise,这意味着 await
和这些方法结合使得
await
可以作用在多个 promise 上。
async function doSomething() {
try {
return await Promise.all([p1, p2, p3])
} catch(e) {
console.error(e.message)
}
}
这里 Promise.all()
方法先执行完毕,然后 await
作用在其结果上。
for-await-of
循环
可迭代对象 (iterable) 是实现了 Symbol.iterator
方法的对象,异步可迭代对象 (async iterable) 是实现了 Symbol.asyncIterator
方法的对象。for-await-of
循环作用在可迭代对象或异步可迭代对象上,如果是异步对象,则等待 promise 成功然后获取其值,再继续下一轮循环。
const p1 = Promise.resolve(2)
const p2 = Promise.resolve(3)
const p3 = Promise.resolve(4)
for await (const v of [p1, p2, p3]) {
console.log(v)
}
// 也可以作用在元素为非 promise 的可迭代对象上,众所周知,这些元素会被隐式转为 promise
for await (const v of [1, 2, 3]) {
console.log(v)
}
Node.js 中最常用的异步可迭代对象是 ReadStream
。 ReadStream
对象用于从可能不可用的源中定期读取数据。如网络请求,读取大文件或事件流。
import fs from "node:fs"
async function readCompleteTextStream(readable) {
readable.setEncoding("utf8")
try {
let data = ""
// 如果可迭代对象的 promise 失败,则 for-await-of 抛出错误
for await (const chunk of readable) {
data += chunk
}
return data
} catch (e) {
console.error(e.message)
}
}
const stream = fs.createReadStream("data.txt")
readCompleteTextStream(stream).then((text) => console.log(text))
顶层 await
表达式
await
表达式还可以用在异步函数之外、 JavaScript 模块内的顶层。从本质上讲,JavaScript 模块在默认情况下充当包裹整个模块的异步函数。这允许你直接调用基于 promise 的函数,比如使用 import()
函数:
// 静态 import
import something from "./file.js"
// 动态 import
const filename = "./another-file.js"
const somethingElse = await import(filename)
使用顶层 await
,可以在静态加载模块的同时动态加载模块。(动态加载的模块也允许我们动态地构造模块说明符,这在静态导入中是不可能的。)
当 JavaScript 引擎遇到顶层 await
时,JavaScript 模块的执行将暂停,直到 promise 执行完毕。如果被暂停模块的父模块有静态导入需要处理,那么那些导入将继续。但在这种情况下,不能保证兄弟模块的加载顺序,不过大多数情况下,该顺序应该无关紧要。
顶层
await
不能用在 JavaScript 脚本中,必须使用import
加载代码或者使用<script type="module">
。
总结
简而言之:
- 异步函数 (
async
) 就是在函数前面添加一个async
关键字,使函数最终返回一个 promise await
就是在一个 promise 前面添加await
关键字,帮我们隐式调用 promise 的 handler (成功调用 resolve,失败调用 reject),然后返回真正的值