Skip to content

AHABHGK

co 与异步的一些思考

SourceCode3 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.6 🔑 the key

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

💫 后记

async/await in babel

可以看到 babel 的编译也是将 async/await 编译成 _asyncToGenerator(generatorFn) 使用 generator 实现,而 generator 则是利用了 regenerator 这个库实现的,这个库的理论支持则是 Continuation

In computer science, a continuation is an abstract representation of the control state of a computer program. A continuation implements (reifies) the program control state, i.e. the continuation is a data structure that represents the computational process at a given point in the process's execution; the created data structure can be accessed by the programming language, instead of being hidden in the runtime environment. Continuations are useful for encoding other control mechanisms in programming languages such as exceptions, generators, coroutines, and so on. --- wikipedia

关于 Continuation 可以看看这些文章:

我接触到 Continuation 的起因是在刷题时又忘了二叉树的遍历用循环的方法怎么写,又觉得很多场景下递归明显更符合人的思维,背这些循环方法可能只是为了应试,于是想找一种通用的迭代转循环的方法,发现了 Continuation 的宝藏,这也是一种 Yak Shaving

ref