自己来写个co

昨天被问到实现 co() (tj/co) 这个很经典的函数,花了几分钟写了下。后来回想下发现有个地方处理得不是很好,故在此小小地总结下。

首先说下 co(gen) 这个函数

简单点说,这个函数能够执行传入的生成器函数 gen ,最后返回一个Promise,当 gen 一切都顺利进行的时候这个 Promise 应该被以起返回值 resolved, 而当有任何异常发生的时候,这个Promise应该被相应地 rejected.

比如正常情况下:

const res = await co(function*(){
    a = yield 1
    b = yield 2
    return 3
})

那么期望的结果则是 a === 1, b === 2res === 3.

此外,co(gen) 还有个特点,就是当 yield 一个 Promise 的时候,co 应该等待这个 Promise 被 resolved 的时候再把 resolved 的值传回 gen,而当其被 rejected 的时候应该要把 rejected 的异常在这个 gen 函数中抛出。

当然真正的co库还会处理 thunk, 基于数组或对象的并行Promise, 还有嵌套的 generator.

最初的版本

function co(gen) {
    return new Promise((resolve, reject) => {
        try {
            const generator = gen()

            const execute = (generator, value) => {
                const x = generator.next(value)
                if (x.done) {
                    resolve(x.value)
                    return
                }

                const nextValue = x.value
                if (isPromiseLike(nextValue)) {
                    nextValue.then(
                        v => execute(generator, v),
                        e => {
                            generator.throw(e) // 出错了,在generator内部抛出异常
                            execute(generator) // 处理被catch的情况
                        }
                    )
                } else {
                    execute(generator, nextValue)
                }

            }

            execute(generator)
        } catch (e) {
            reject(e)
        }
    })
}

现在来看,这个版本能够处理基本的 yield 一个普通值流程,也能处理 yield 一个 Promise 的情况,甚至即使在 gen 函数刚开始被调用的时候抛出了异常的场景下也能如期返回一个被 rejected 的 Promise.
但是,有两个问题:
1. generator.throw() 的返回值没有处理
2. generator.next() 以及 generator.throw() 抛出的异常没有处理

首先,先来看下 generator.throw()说明

语法
gen.throw(exception)
参数 exception – 用于抛出的异常。 使用 Error 的实例对调试非常有帮助.
返回值
带有两个属性的对象:
– done(boolean)
– 如果迭代器已经返回了迭代序列的末尾,则值为 true。在这种情况下,可以指定迭代器 value 的返回值。
– 如果迭代能够继续生产在序列中的下一个值,则值为 false。 这相当与不指定 done 属性的值。
– value – 迭代器返回的任何 JavaScript 值。当 done 是 true 的时候可以省略。

光看文档说明,我们能知道,其实这个 generator.throw(e)generator.next(v) 的返回值是一样的,其实都应该在 co() 内进行处理,根据返回的对象的 done 属性来决定要不要继续处理。
可是,文档没有提到的是: gen() 函数内抛出了一个异常,generator.throw(e)generator.next(v) 都会抛出异常的。

改进版本

让我们处理下generator.throw() 的返回值,并处理下 generator.next() 以及 generator.throw() 抛出的异常:

function co(gen) {
    return new Promise((resolve, reject) => {
        try {
            const generator = gen()

            const execute = (generator, value) => {
                let x
                try {
                    x = generator.next(value)
                } catch (e){
                    reject(e)
                }

                if (x.done) {
                    resolve(x.value)
                    return
                }

                const nextValue = x.value
                if (isPromiseLike(nextValue)) {
                    nextValue.then(
                        v => execute(generator, v),
                        e => {
                            try {
                                const t = generator.throw(e) // 出错了,在 gen 内部抛出异常
                                if (t.done){
                                    resolve(t.value)
                                } else {
                                    execute(generator, t.value) // 处理被 catch 的情况
                                }
                            } catch (e) {
                                reject(e)
                            }
                        }
                    )
                } else {
                    execute(generator, nextValue)
                }
            }

            execute(generator)
        } catch (e) {
            reject(e)
        }
    })
}

OK,这下子才算真正搞定了。来执行下基本的单元测试:

> mocha --require intelli-espower-loader

  Test co()
    √ Simple generator should be ok
    √ Thrown errors should be rejected
    √ Thrown errors should be rejected even after yield
    √ Resolved Promise should be expanded
    √ Rejected Promise should be thrown inside generator
    √ Rejected Promise should be thrown if uncaught


  6 passing (26ms)

Great! All passed.

附:详细代码见 clarence-pan/my-co

当然,这个 co 也还不是完整的 tj/co,还缺少以下功能:

  • 对 thunk 函数的支持
  • 对基于数组或对象的并发流程的支持
  • 对于嵌套的generator的支持

后续有时间再补充吧。

打赏

自己来写个co》上有2条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注