Skip to content

AHABHGK

co 与异步的一些思考

SourceCode2 min read

Table of Contents

JavaScript 的异步编程发展经过了四个阶段:

  1. 回调函数、发布订阅

  2. Promise

  3. co 自执行的 Generator 函数

  4. async / await

第三阶段现在基本不用了,但也起到了承上启下的作用

📝 co 源码

co 接收一个 generator 函数,返回一个 promise,generator 函数中 yieldable 对象有:

  • promises

  • thunks (functions)

  • array (parallel execution)

  • objects (parallel execution)

  • generators (delegation)

  • generator functions (delegation)

其中 array 和 objects 是并行执行的,里面的值仍然是 promise 和 thunk 函数,而 generators 和 generator functions 是通过代理执行,内部再次调用 co,所以简单来说都是基于 promise 和 thunk 函数的,而 co 内部对于 thunk 的处理是把 thunk 也转化成 promise,所以直接看对于 yield 一个 promise 的 generator 怎么自动执行

const gen = gen()
function* gen() {
const foo = yield Promise.resolve(1)
const bar = yield Promise.resolve(2)
console.log(foo)
console.log(bar)
}
const gen = gen()
g.next().value.then((data) => {
}
g.next().value.then((data) => {
}
g.next().value.then((data) => {
g.next(data).value.then((data) => {
}
g.next(data).value.then((data) => {
}

这里我们写一个 GeneratorFunction,每次都 yield 出一个 promise,我们如何让这段代码以类似同步的执行方式从上到下执行

先得到一个 generator,然后 next,此时 generatorFunction 执行到 yield 处

返回的结果的 value 是 yield 出来的 promise 容器包裹的数值 1,那么 then 方法的 callback 的参数就是 1

为了让 yield 左边的变量 foo 得到异步代码的结果,我们只需要把 data 通过 generator 的 next 方法传入就可以了,同时 generatorFunction 的控制权也回到 generatorFunction 手中,generatorFunction 继续执行

之后再次 yield 出 promise 的异步操作,交出控制权,同样的通过 next 返回结果和控制权让 generatorFunction 继续执行,这样就实现了包含异步操作的 generatorFunction 的同步执行

在直接套用 co 源码:

1function co(gen) {
2 // ...
3 return new Promise((resolve, reject) => {
4 const g = gen()
5
6 const gResult = g.next() // 第一次 next
7 if (gResult.done) resolve(gResult.value)
8 if (gResult.value && isPromise(gResult.value)) {
9 value.then((res) => {
10
11 const gResult = g.next(res) // 第二次 next
12 if (gResult.done) resolve(gResult.value)
13 if (gResult.value && isPromise(gResult.value)) {
14 value.then((res) => {
15
16 const gResult = g.next(res) // 第三次 next,done 为 true
17 if (gResult.done) resolve(gResult.value) // resolve 掉 generator 中 return 的结果
18 })
19 }
20 })
21 }
22 })
23}

在看 co 整体代码:

1function co(gen) {
2 var ctx = this; // 那 this,一般是 co.call 这样调用
3 var args = slice.call(arguments, 1) // generator 的参数可以在 gen 后面传入
4
5 return new Promise(function(resolve, reject) {
6 // 检查 gen
7 if (typeof gen === 'function') gen = gen.apply(ctx, args); // 普通函数就会调用得到返回值,下一行 resolve 返回值
8 if (!gen || typeof gen.next !== 'function') return resolve(gen);
9
10 onFulfilled();
11
12 function onFulfilled(res) {
13 var ret;
14 try {
15 ret = gen.next(res);
16 } catch (e) { // try / catch 做错误捕获
17 return reject(e); // 出错就 return reject 掉,return 是为了防止 reject 后仍然执行 next 函数
18 }
19 next(ret);
20 }
21
22 function onRejected(err) {
23 var ret;
24 try {
25 ret = gen.throw(err);
26 } catch (e) {
27 return reject(e);
28 }
29 next(ret);
30 }
31
32 function next(ret) {
33 if (ret.done) return resolve(ret.value); // new Promise 的 resolve 用来 resolve 最终 done 为 true 时的 value
34 var value = toPromise.call(ctx, ret.value); // 把其他的 yieldable 转化成 promise
35 if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
36 return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
37 + 'but the following object was passed: "' + String(ret.value) + '"'));
38 }
39 });
40}

其中 toPromise 针对不同的 yieldable 进行 xxxToPromise,arrayToPromise 是通过 Promise.all(value.map(toPromise)) 进行转换,objectToPromise 等待对象的所有的值都 resolve 后,并添加到新的对象中,然后再 resolve,类似于 Promise.all

thunkToPromise 类似于一般 Node.js 的 API 的 promisify,只不过是 thunk 函数已经传入了第一个参数,promisify 时只需要传入另一个参数就可以了,我们也可以看出这里 thunk 是针对 Node.js 的 API 的,与 curry 的不同在于 thunk 是分为两次参数传入的

1function thunkToPromise(fn) {
2 var ctx = this;
3 return new Promise(function (resolve, reject) {
4 fn.call(ctx, function (err, res) {
5 if (err) return reject(err);
6 if (arguments.length > 2) res = slice.call(arguments, 1);
7 resolve(res);
8 });
9 });
10}

isPromise 的判断也是通过查看参数的 then 是不是一个函数,体现了鸭子类型的特点

1function isPromise(obj) {
2 return 'function' == typeof obj.then;
3}

⚙️ 原理

co 的原理其实是通过 generator.next() 得到 generatorResult,由于 yield 出是一个 promise,通过 generatorResult.value.then 再把 promise 的结果通过 generator.next 的参数传给 yield 的左边,让 generator 自动执行,通过 generatorResult.done 判断是否执行结束

🍬 async / await

async / await 是语法糖,我们还原一个 async 函数,使用 TypeScript 跟更体现一些类型本质的东西

const getData = async(function * (url: string) {
type ExtractType<T> =
T extends {
[Symbol.iterator](): {
next(): { done: true, value: infer U }
}
} ? U :
T extends {
[Symbol.iterator](): {
next(): { done: false }
}
} ? never :
T extends {
[Symbol.iterator](): {
next(): { value: infer U }
}
} ? U :
T extends {
[Symbol.iterator](): any
} ? unknown :
never
type Async =
<F extends (...args: any[]) => Generator<unknown>>(fn: F)
=> (...args: Parameters<F>) => Promise<ExtractType<ReturnType<F>>>
function next(nextFn: () => IteratorResult<unknown>) {
const getData = async(function * (url: string) {
function next(nextFn: () => IteratorResult<unknown>) {
function next(nextFn: () => IteratorResult<unknown>) {
function next(nextFn: () => IteratorResult<unknown>) {

先对类型进行编写,async function 返回一个 Promise,Promise 包裹内部 return 的值,由于我们模拟 Async 函数要传入一个 GeneratorFunction,返回的一个函数才相当于 async function,所以通过 ExtractType 拿到 Generator<unknown> 最终 done 为 true 时的 value 的类型

我们实现的用法就想这样,除了 () * yield 写法不一致其他与 async function 用法一样,与 co 不同的是 yield 后可以跟任何值,不止是 Promise

(...args) => new Promise(...) 相当于我们实际调用的 async function,通过 thunk 和展开运算符把 genFn 的参数拿到,并在传入 genFn,得到 gen

同时我们要在 next 内部执行 gen.next,通过包裹一个函数 nextFn 传入,在内部得到 result

通过判断是否 done 进行 new Promise 的 resolve,如果没有完成就继续通过 next 进行传递,注意不同于 co 我们内部用 Promise.resolve 处理 result.value,所以我们 yield 时也可以不是一个 Promise

之前的标准是使用 new Promise(res => res(resule.value)) 进行包裹处理,v8 提出 Faster async functions and promises 并 PR,现在已经修改为 Promise.resolve

对于这两个的区别在于 resolve 一个 Promise 时的表现不同,Promise.resolve(p) 对于 Promise 会直接返回这个 Promise,而 new Promise(res => res(p)) 在内部调用 p.then(resolve, reject) 相当于多出一个微任务来处理 res(p),所以目前新版的更快,有些代码执行顺序也会不同

现在我们加上错误处理,当 resolve value 出错时会通过 gen.throw(err) 抛出错误,而 gen.throw 通过 genF 内部的 try / catch 捕捉(所以 async / await 的错误处理一般也是在函数内写 try / catch)然后通过上面的 try / catch 将错误 reject 出来,不同于成功时 async 函数返回一个包裹 value 的 Promise,而是返回出一个包裹 error 的 Promise

现在我们完成和 async / await 的函数的模拟,我们看到 async function 实际上返回一个 Promise 包裹的 return 值,await 会自动使用 Promise.resolve 进行包裹,并类似 yield 把 flat 后得到的结果代替那个表达式

这个函数与 co 的不同除了使用 Promise.resolve 自动包裹,不能处理 yield 数组和对象时实现的并行以外,还有将 gen.next 和 gen.throw 抽象成 nextFn,这也导致直观上代码行数不同,但本质实际上没有什么区别

🤔 对于 JavaScript 异步的思考

3.1 raw callback

我们看最开始最朴素的 raw callback,是将 callback 交给另一个函数执行,也就是说我们把 callback 的控制权交给这个函数,这个函数在进行完异步操作之后调用 callback,以此实现异步

3.2 Promise callback

而之后 promise 也是通过传入 callback 的方式,只不过把之前嵌套式的形式展开成链式,其实通过链表为函数增加 next 属性,也可以使嵌套式展开成链式

promise 通过完成异步操作后进行 resolve 或 reject,来控制 callback 的执行,而且提供了 then 返回一个 promise 的自动进行 flat(flatMap),实现了 then 中继续执行异步的操作,所以提供 callback 参数对于 promise 来说也是一种控制权的转移,只不过是从以前直接的函数调用改成了 resolve、reject 控制 callback 的调用时机

同时是一种标准的实现也相较于原来的 raw callback 保证了内部的可控性与安全性

3.3 co + Generator

GeneratorFunction 得到的 Generator 可以通过 next 打断 GeneratorFunction 的执行,由于只能通过 Generator 调用 next 把 GeneratorFunction 的执行权还给 GeneratorFunction,所以称作“半协程”

通过保存 GeneratorFunction 的执行上下文,使 GeneratorFunction 可中断执行,从而把 GeneratorFunction 控制权交给 Generator,Generator 拿到控制权后通过 yield 出来的 promise 完成异步操作,等 resolve 之后再通过 then 中调用 next 把异步的结果和 GeneratorFunction 的控制权交给 GeneratorFunction,以继续执行 yield 后的操作

3.4 async / await

async 函数是对 GeneratorFunction + co 的语义化和标准化的语法糖

便捷性提升的同时也意味着灵活性的减少,由于 async / await 是语法,而 promise、callback 是对象,对象可以到处传递,React 也通过 throw 一个 promise 如此 creative and hacking 的模拟了 Algebraic Effects 实现 Suspense

同时 Promise 和 GeneratorFunction 也相对于 raw callback 约束,Promise 是 onFulfilled、onRejected 的约束,GeneratorFunction 是 next、done 的约束,Node.js APIs 中也限制了 cb 的参数,所以也能被统一的 thunk 化,这种约束类似于语法糖,规范的同时也丧失了些许灵活性

Promise 作为 async function 中的异步最小单位通过 await 进行传递,而 Promise 又是由 callback 组成,所以 co + Generator(async / await)也是一种 callback 的形式,只不过写法更加方便规范

3.5 RxJS

// TODO: RxJS 与 async 区别,RxJS 理念等

3.6 🔑 the key

异步的关键就在于调用 callback 的时机,因为我们不知道异步操作需要多少时间,我们自然也就不知道何时调用异步之后的操作,所以我们通过 callback 将之后操作的控制权交给异步操作,实现控制反转,在异步操作完成之后自动调用 callback,就完成了在合适的时机进行合适的操作

ref

Generator 函数的异步应用

100 行代码实现 Promises/A+ 规范

JAVASCRIPT GETTER-SETTER PYRAMID