愿你坚持不懈,努力进步,进阶成自己的巨人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之async函数

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)返回Promiseasync函数返回的是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().donetrue

  • 如果报错,则整个Promise的状态变为rejected。如果donetrue,则整个Promise的状态变为resolved
  • Promise.resolve()包装value,当异步操作完成后继续调用gen.next(),并将异步操作的返回值作为参数传给gen.next()。如果异步操作失败,则调用gen.throw()


五、异步遍历器

Iterator接口是一种数据遍历的协议。当我们调用一个遍历器对象的时候,执行next()方法,就会得到一个对象(数据结构为{value, done}),而这里面隐含了一点:next方法必须是同步的,即一调用next()方法,就必须立即返回一个值。
如果是同步操作,自然没有什么问题,但若是异步操作就不太合适了,因为异步操作没有办法立即返回value,目前变通的方案则是返回一个thunk函数或者Promise对象
目前,有提案:异步遍历器,它为异步操作提供了原生的遍历器接口,和普通遍历器不同的是它的valuedone两个属性都是异步产生的

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)
    }
})()