async & await 原理:generator + co

231 阅读2分钟

generator 基本语法和特点

*yeild

function* read() {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5
}

我们运行下面代码

let it = read()
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())
console.log(it.next())

得到结果,利用 next 进行迭代,特点就是可以暂停,只要遇到一个 yield 就暂停一次,一开始read 的时候是遇到第一个 yield 被暂停了, 可以迭代到 done 为 true

generator 的应用: 生成迭代器

如果我们想把一个普通的对象转换成一个数组,我们尝试用 Array.from(obj)

let obj = {
    0: 1,
    1: 2,
    length: 2
}
console.log(Array.from(obj))

发现可行 再尝试用 [...obj]

let obj = {
    0: 1,
    1: 2,
    length: 2
}
console.log([...obj])

会发现报错,提示 obj 不是可迭代的 那么我们硬是要用 [...obj] 来将对象转成数组呢,我们就需要对对象进行改造,使其编程可迭代的。我们需要提供一个迭代的接口,迭代器是一个对象,并且有一个 next 方法,next 方法每次返回{value:x,done:true/false}

首先,我们自己模拟一个迭代器

let obj = {
    0: 1,
    1: 2,
    [Symbol.iterator]() { //可迭代的方法
        let index = 0
        return {
            next: () => { //如果不用箭头函数,函数内部的 this 不是pbj
                return {
                    value: this[index], //这个this是构造对象前的this,对象被构造完才会有自己的this
                    done: this.length === index++
                }
            }
        }
    }, //元编程,修改js 的行为
    length: 2
}
console.log([...obj])

接下来我们利用 gnerator 的特点,利用其产生迭代器

let obj = {
    0: 1,
    1: 2,
    *[Symbol.iterator]() { //可迭代的方法
        for (let i = 0; i < this.length; i++) {
            yield this[i]
        }
    }, //元编程,修改js 的行为
    length: 2
}
console.log([...obj])

generator 与 Promise 结合使用

首先来看这样一段代码

function* read() {
    let a = yield 'hello'  
    console.log(a)
    let b = yield 'world'
    console.log(b)
}
let it = read()
console.log(it.next())
console.log(it.next())
console.log(it.next())

其结果是 说明 a 和 b 都没有被赋值,我的理解是这样的,可以理解为下面这段代码(为个人揣测, 未考证过,错误请指正!)

function* read() {
    let a
    //赋值被暂停
    yield 'hello'  
    console.log(a)
    let b
    //赋值被暂停
    yield 'world'
    console.log(b)
    //结束
}

那么我们如何对 a 和 b 进行赋值嗯?可以在 next 中传值,传递给作为上一次 yield 的返回值

let it = read()
console.log(it.next()) //第一次 next 传参是无意义的
console.log(it.next(1)) //会传递给上一次 yield 的返回值
console.log(it.next(2))

如果我们现在有一个需求,就是读取一个文件,这个文件中有下一个需要读取的文件的地址,我们创建一个 read 迭代器

let fs = require('fs').promises //读取完的结果是 Promsie

function* read() {
    let content = yield fs.readFile('./name.txt', 'utf8')
    let r = yield fs.readFile(content, 'utf8')
    return r
}

显然,我们可以通过 next 来传递上一次 yield 表达式读到的地址给等号左边进行赋值(实现了异步赋值),并给下一次读取提供地址,有一点绕。

let it = read()
let {
    value,
    done
} = it.next()
//保证是 Promise
Promise.resolve(value).then(data => {
    let {
        value,
        done
    } = it.next(data)
    Promise.resolve(value).then(data => {
        let {
            value,
            done
        } = it.next(data)
        console.log(value)
    })
})

可以看到这其实是一种嵌套,这种写法比较丑,我们应该把它改成递归的形式,有一个第三方库 co,可以看一看。我们自己来简单实现一下自己的 co 函数。

co 简单实现

我们希望实现的效果是,执行一次就能得到嵌套读取的最后结果

co(read()).then(data => {
    console.log(data)
}, err => {
    console.log(err)
})

co 简单实现:

function co(it) {
    return new Promise((resolve, reject) => {
        function next(data) {
            let {
                value,
                done
            } = it.next(data)
            //按照 done 状态进行递归,直到为 true
            if (!done) {
                Promise.resolve(value).then(data => {
                    next(data) //给上一个 yield 返回值
                }, reject)
            } else {
                resolve(data)
            }
        }
        next() //第一次不需要传
    })
}

async & await

要实现 generator + co 的效果,我们只需要用 async 和 await 这个语法糖即可,它会让异步代码看起来像是同步执行的。

async function read() {
    try {
        let content = await fs.readFile('./name.txt', 'utf8')
        let r = await fs.readFile(content, 'utf8')
        return r
    } catch (e) {
        console.log(e)
    }
}