ES7引入了async函数,使得异步操作变为更加方便和简单。async本质上是generator函数的语法糖
一、介绍
我们现在需要依次读取两个文件,使用generator写,如下:
const fs = require('fs')
const readFile = function(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) {
return resolve(error)
}
resolve(data)
})
})
}
const gen = function* () {
const f1 = yield readFile('some/file1')
const f2 = yield readFile('some/file2')
console.log(f1)
console.log(f2)
}
co(gen)
而如果改写成async函数的话,则为:
const asyncReadFile = async function() {
const f1 = await readFile('some/file1')
const f2 = await readFile('some/file2')
console.log(f1)
console.log(f2)
}
和原始的generator相比,async
函数的好处在于:
1)内置执行器
,所以可以直接执行,像普通函数一样调用,如:asyncReadFile()
2)更好的语义
,async/await
语义清晰,而且写法简洁,能够使得代码更易于理解
3)更广的适用性
,相比co
模块,yield
命令后面接的需要为Thunk
函数或者Promise
对象,而await
后可以接Promise对象或者原始数据类型(接原始数据类型时相当于同步操作)
4)返回Promise
,async
函数返回的是Promise
对象,因此我们可以对async
函数的返回值使用then
来指定下一步的操作
二、基础用法
1)async
函数执行时,遇到await
就会先返回,等到await
后的异步操作执行完毕后再执行后面的语句
2)async
函数返回一个Promise,所以可以使用then
添加回调函数
例子如:
async function getStockPriceByName(name) {
const s = await getStockSymbol(name)
const price = await getStockPrice(s)
return price
}
getStockPriceByName('goog').then(res => console.log(res))
下面是另一个例子,如:
function timeout(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('Hello, world', 2000)
由于async
函数返回的是Promise对象,所以可以作为await
的参数,因此以上例子可以改为:
async function timeout(ms) {
await new Promise(resolve => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms)
console.log(value)
}
asyncPrint('Hello, world', 2000)
async函数有多种形式,如:
1)函数声明:async function foo() {}
2)函数表达式:const foo = async function() {}
3)对象的方法:
let obj = {
async foo() {
}
}
4)类的方法:
class SomeClass {
async foo() {
// ...
}
}
5)箭头函数:
const foo = async () => {
// ...
}
三、语法
1、返回Promise
async
函数返回的是一个Promise
对象,其return
语句返回的值,会成为then
回调函数的参数,而内部抛出的错误,会导致返回的Promise
对象变为reject状态,从而被catch
方法捕捉,如:
async function foo() {
return 'Hello, world'
}
foo().then(console.log) // 输出:Hello, world
async function bar() {
throw 'some errors'
}
bar().catch(e => console.log(e)) // 输出:some errors
2、状态转化
async
函数返回的Promise对象,必须等到内部所有await命令后接的Promise对象都执行完后,才会发生状态转变,除非遇到return
或者抛出了错误,所以有:
async function getHTML(url) {
let response = await fetch(url)
let html = await response.text()
return html
}
getHTML(someUrl).then(console.log) // 输出对应的HTML文本
在上述例子中,只有当fetch()
、response.text()
两个操作都完成了,then
里的回调函数才会执行
3、await命令
1)await
后接的是一个Promise对象(如果不是Promise对象,会转成Promise对象)
2)async
函数里所有await
后接的Promise对象里,只要有一个rejected了,那么整个async
函数就会终止执行,并且返回的Promise状态为rejected,如:
async function f() {
await Promise.reject('出错了')
await Promise.resolve('Hello, world') // 不会执行
}
如果我们不希望前一个异步操作失败会中断后面的异步操作,那么可以使用try-catch
块包裹:
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {
}
return await Promise.resolve('Hello, world')
}
或者后面跟一个catch
方法,如:
async function f() {
await Promise.reject('出错了').catch(e => console.log(e))
return await Promise.resolve('Hello, world')
}
4、注意点
1)多个await命令,如果不存在继发关系,最好是写成同时触发,如:
async function f() {
let [foo, bar] = await Promise.all([getFoo(), getBar()])
}
// 也可以是先启动两个promise,再await结果:
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise
2)await
只能用在async
函数中,如果放在普通函数里,会出错,如:
async function f() {
let urls = [url1, url2, url3]
urls.forEach(function(item) {
await fetch(item)
}) // 报错
}
如果此时将forEach的回调函数改为async function()
的形式,也不对,虽然不会报错,但是此时是并发关系
,而非继发关系,所以正确的做法是采用for
循环,如:
async function f() {
let urls = [url1, url2, url3]
for (let url of urls) {
await fetch(url)
}
}
如果想要写并发关系的请求,则可以使用Promise.all
,如:
async function f() {
let urls = [url1, url2, url3]
let promises = urls.map(url => fetch(url))
let results = await Promise.all(promises)
console.log(results)
}
它等同于以下的写法:
async function f() {
let urls = [url1, url2, url3]
let promises = urls.map(url => fetch(url))
let results = []
for (let promise of promises) {
results.push(await promise)
}
console.log(results)
}
3)采用@std/esm
模块加载器,可以支持顶层await
四、async函数的实现原理
async函数的实现原理,就是将generator函数和自动执行器,包装在一个函数里,如:
async function fn(args) {
// ...
}
它相当于:
function fn(args) {
return spawn(function* () {
// ...
})
}
而spawn
就是自动执行器,它的实现如下:
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF()
function step(nextF) {
let next
try {
next = nextF()
} catch(e) {
return reject(e)
}
Promise.resolve(next.value).then(function(v) {
step(function() {
return gen.next(v)
})
}, function(e) {
step(function() {
return gen.throw(e)
})
})
}
step(function() {
return gen.next(undefined)
})
})
}
实例说明,以下面例子为例:
async function foo() {
let a = await someOperation1()
let b = await 2
return 3
}
在包装后,得到:
function foo() {
return spawn(function* () {
let a = yield someOperation1()
let b = yield 2
return 3
})
}
而执行spawn
函数,过程如下:
1)spawn
函数返回一个Promise(符合async函数返回Promise的特征)
2)首先拿到generator函数,并执行,得到一个遍历器对象gen
3)内部step()
的作用是不断调用gen.next()
,直到gen.next().done
为true
:
- 如果报错,则整个Promise的状态变为rejected。如果
done
为true
,则整个Promise的状态变为resolved
- 用
Promise.resolve()
包装value
,当异步操作完成后继续调用gen.next()
,并将异步操作的返回值作为参数传给gen.next()
。如果异步操作失败,则调用gen.throw()
五、异步遍历器
Iterator接口是一种数据遍历的协议。当我们调用一个遍历器对象的时候,执行next()
方法,就会得到一个对象(数据结构为{value, done}
),而这里面隐含了一点:next
方法必须是同步的,即一调用next()
方法,就必须立即返回一个值。
如果是同步操作,自然没有什么问题,但若是异步操作就不太合适了,因为异步操作没有办法立即返回value
,目前变通的方案则是返回一个thunk
函数或者Promise
对象
目前,有提案:异步遍历器
,它为异步操作提供了原生的遍历器接口,和普通遍历器不同的是它的value
和done
两个属性都是异步产生的
1、基本知识
异步遍历器最大的特点,就是调用next
方法时,返回的是一个Promise对象,而非立即返回{value, done}
,即:
asyncIterator.next().then(({value, done}) => {
// ...
})
同步遍历器有接口Symbol.iterator
,而相应的,异步遍历器也有接口Symbol.asyncIterator
,只要一个对象的Symbol.asyncIterator
有值,就表示应该对其进行异步遍历:
const asyncIterable = createAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.asyncIterator]()
asyncIterator.next()
.then(res1 => {
console.log(res1) // 输出:{ value: /* value of res1 */, done: false }
return asyncIterator.next()
})
.then(res2 => {
console.log(res2) // 输出:{ value: /* value of res2 */, done: false }
return asyncIterator.next()
})
.then(res3 => {
console.log(res3) // 输出:{ value: /* value of res3 */, done: true }
})
总结而言:异步遍历器
和同步遍历器
最终行为是一致的,只不过异步遍历器
中间会先得到一个Promise
对象作为中介。而由于异步遍历器的next()
方法返回的是一个Promise
对象,因此可以将其放在await
命令后面:
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.asyncIterator]()
let a = await asyncIterator.next()
let b = await asyncIterator.next()
let c = await asyncIterator.next()
}
此外,next()
方法是可以连续调用的,可以不必等到上一步Promise的状态变为resolved后再调用,可以这么写并发的调用:
const [{value: v1}, {value v2}] = await Promise.all([
asyncGenObj.next(),
asyncGenObj.next()
])
2、for await-of
for-of
用于遍历同步的Iterator接口,而for await-of
则用于遍历异步的Iterator接口,如:
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x)
}
}
// a
// b
其中, createAsyncIterable()
返回的是一个异步遍历器,for-of
会自动循环调用这个遍历器的next()
方法,然后得到一个Promise
对象,而await
则处理这个Promise对象,当其状态resolved的时候,就将得到的值作为x传入for-of
循环体。所以部署了asyncIterable
接口的异步操作,可以直接放入这个循环:
let body = ''
async function f() {
for await (const data of req) {
body += data
}
const parsed = JSON.parse(body)
}
注意: for await-of
里抛出的错误,可以用try-catch
捕获。同时,for await-of
循环也可以用于同步遍历器
3、异步generator函数
同步generator函数返回一个同步遍历器,而异步generator则返回一个异步遍历器对象
4、异步generator的yield*
yield*
也可以跟一个异步的遍历器,如:
async function* gen1() {
yield 'a'
yield 'b'
return 2
}
async function* gen2() {
const result = yield* gen1() // 最终得到的值为:2
}
和同步generator类似,异步generator的yield*
相当于调用for await-of
:
(async function () {
for await (const x of gen2()) {
console.log(x)
}
})()