迭代器: Iterators are just objects with a specific interface designed for iteration. All iterator objects have a next() method that returns a result object.
生成器: A generator is a function that returns an iterator.
可迭代对象: Closely related to iterators, an iterable is an object with a Symbol.iterator property.
co 最新版是 v4.6.0,是 2015-07-09 发布的,距今已经 6 年多了,可见已经比较稳定,或者说现在基于 async/await 编程的话,根本是不需要 co 这个东西的。他的 README.md 也说了,对于 co@4.0.0 「It is a stepping stone towards the async/await proposal.」,返回类型用 Promise 替代了原来的 “thunk”。类型定义 @types/co 最后一次更新是在 2019-06-05,比较老旧。
type ExtractType<I> = I extends { [Symbol.iterator]: () => Iterator<any, infer TReturn, any> } ? TReturn : I extends (...args: any[]) => any ? ReturnType<I> : I
// 注意 TS 里面的 Generator 继承自 Iterator,本质上还是一个可迭代对象,GeneratorFunction 才是生成函数的类型定义 // yield number, return string, next boolean type A = ExtractType<Generator<number, string, boolean>> // yield number, no return, next boolean type B = ExtractType<Generator<number, undefined, boolean>> // no yield number, return string, next boolean type C = ExtractType<Generator<undefined, string, boolean>>
function * d (x: number, y: string, z: boolean): Generator<boolean, string, number> { const ret = yieldfalse console.log('a', ret) return'1' }
type D = typeof d // 把 D 作为参数传给 Co 的时候,看一下返回类型 type E = ReturnType<Co<D>>
type F = () => Generator<boolean, undefined, number> type G = ReturnType<Co<F>> type H = () => Generator<undefined, string, number> type I = ReturnType<Co<H>> // 可以看到最终 Co 函数的返回类型就是 Promise<yield | return> exporttype coCases = [ Expect<Equal<E, Promise<string>>>, Expect<Equal<G, Promise<undefined>>>, Expect<Equal<I, Promise<string>>>, ]
简单来说,在 co 包裹的 generator 内部连续使用 yield 的语法跟在 async 函数中连续使用 await 是一样的,async/await 是在 2017 年也就是 ES2017 (ES8) 正式发布的,比 co 出现还是晚很久(co 刚开始应该 Promise 都还没出来,用的是文档里面提到的 “thunk” 函数,就是一个带回调的函数 function (callback) {...},这个可以参考《深入理解 ES6》的「异步任务执行器」一节的回调的代码模式)。所以可以理解为 generator function + co + promise = async/await。
co 会使用传入的生成器函数创建一个迭代器,然后遍历这个迭代器,每次拿到迭代器 yield 出来的值 ret,就调用迭代器的 .next(ret) 将 ret 赋值给上面样例代码中的 r1/r2(可以参考《深入理解 ES6》的「给迭代器传递参数」和「向任务执行器传递数据」 这两节,这个逻辑和书上的样例是一模一样的)。迭代器结束的(done=true)时候,得到的值(也就是生成器函数里的 return 语句)作为 Promise 最后 resolve 的值。
co 实现代码
co 函数只有 60 行左右,非常简单,我加了一些注释:
functionco(gen) { // 保存 this 指针 var ctx = this; // 保存 gen 后面所有参数 var args = slice.call(arguments, 1);
// we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 returnnewPromise(function(resolve, reject) { // 如果 gen 是函数(注意 typeof generator function === 'function'),则执行这个函数 // 显然对于 gen 是 generator function 的场景,赋值之后 gen 就变成了一个可迭代对象,后续可以和 yield 交互了 // 如果传入的 gen 是一个普通函数,赋值之后 gen 就变成了这个函数的返回值 if (typeof gen === 'function') gen = gen.apply(ctx, args); // !gen 或者 gen.next 不是函数(也就是说 gen 不是一个可迭代对象的情况),直接 resolve gen // 其实我觉得这里更准确的方法应该是判断 typeof gen[Symbol.iterator] !== 'function',因为生成器返回的迭代器(既是一个迭代器(有 `next` 方法)、又是一个可迭代对象(有 `Symbol.iterator` 属性))一定都有 `Symbol.iterator` 属性,参考 https://stackoverflow.com/a/32538867/8242705 // 其实也可以看 TS 类型定义 `Generator` 继承了 `Iterator`,并在其基础上多了一个 `Symbol.iterator` 属性 if (!gen || typeof gen.next !== 'function') return resolve(gen);
传统的基于回调函数的异步模式写起来很不好,容易造成回调地狱,co 相当于一种语法糖,并且如作者所说,「It is a stepping stone towards the async/await proposal.」。co 利用 generator function 中的 yield 和 next 可以让异步代码编写起来非常自然,当如今 Promise 和 async/await 加入 ES 新标准多年,都已经非常成熟了,co 也就逐渐不怎么需要了,但是我们从中可以看到 async/await 其实是可以用 generator function 来做 polyfill 的。
co 里面最主要的工具函数主要是递归的实现 toPromise,对嵌套对象可以进行递归转换,以后有需要可以借鉴。