考古向:Generator 是怎么实现异步的
前言
之前在看 ahook 的useAsyncEffect实现的时候,注意到useAsyncEffect这个钩子是支持AsyncGenertaor的。AsyncGenertaor就是支持async修饰符的Generator,在 ES2018(ES9)提出,详见文档。
下面举个栗子,下面的代码的代码相当于循环调用generate().next(),generate中的异步代码会串行地执行。
js- function delayedValue(time, value) {
- return new Promise((resolve /*, reject*/) => {
- setTimeout(() => resolve(value), time);
- });
- }
- async function* generate() {
- yield delayedValue(2000, 1);
- yield delayedValue(100, 2);
- yield delayedValue(500, 3);
- yield delayedValue(250, 4);
- yield delayedValue(125, 5);
- yield delayedValue(50, 6);
- console.log("All done!");
- }
- async function main() {
- for await (const value of generate()) {
- console.log("value", value);
- }
- }
- main().catch((e) => console.error(e));
Promise的提出是为了解决异步任务回调地狱造成代码可读性大幅下降的问题,即使是有了Promise,仍然需要把异步任务的执行嵌套在then方法中,回调地狱并没有被完美的解决。我们需要一个能处理嵌套逻辑的工具,这也是Generator的作用。
…然后突然想起自己似乎并不清楚没有async的情况下,Generator是如何支持异步的流程控制的,于是就有了本文。
基于 Thunk 的 Generator 流程控制
Thunk
Thunk 其实是对传名调用的一种实现,函数不会对表达式的入参进行计算,而是在函数执行时才进行计算。这里我们说的 Thunk 其实是一个使得函数可以先储存一部分参数的高阶函数,具体实现如下(详见 thunkify 的源码):
js- function thunkify (fn) {
- return function () {
- let args = new Array(arguments.length)
- let ctx = this
- for(var i = 0; i < args.length; ++i) {
- args[i] = arguments[i]
- }
- return function(callback){
- let called
- args.push(function () {
- if (called) return
- called = true
- callback.apply(null, arguments)
- })
- try {
- fn.apply(ctx, args)
- } catch (err) {
- callback(err)
- }
- }
- }
- }
这里有一个异步任务:
js- const cat = (sound, name, fn) => {
- window.setTimeout(() => {
- const data = {
- sound,
- name
- }
- fn(data)
- }, 1000)
- }
我们可以把除了回调函数以外的参数先传到thunkify中:
js- const catThunk = thunkify(cat)
- const thunkFunc = catThunk('miao', 'daijin')
例如传入回调函数:
js- thunkFunc(console.log)
运行这段代码,控制台就会在 1s 后打印:
cmd- { sound: 'miao', name: 'daijin' }
Generator 的异步流程控制
于是我们知道了 Thunk 的使用,那和Generator有什么关系呢?要让异步任务串行执行,只需要在一个异步任务的回调函数中,执行下一个异步任务即可,嵌套太多就形成回调地狱了,这里我们将嵌套执行异步任务的逻辑抽取出来,让代码看起来是“同步”的,看下面的代码:
js- const run = gen => {
- const g = gen()
- const next = data => {
- try {
- let result = g.next(data)
- if (result.done) return result.value
- result.value(next)
- } catch (e) {
- g.throw(e)
- }
- }
- next()
- }
- const catThunk = thunkify(cat)
- const catThunk1 = thunkify(cat)
- function *gen () {
- const daijin = yield catThunk('miao', 'daijin')
- console.log(daijin)
- const sadaijin = yield catThunk1('aowu', 'sadaijin')
- console.log(sadaijin)
- }
- run(gen)
const g = gen()每次执行next方法,返回一个thunkify包装后的函数result.value。在run函数中,把回调函数传入result.value,回调函数会在cat函数回调执行时(类似于Promise的then方法),执行生成器g的下一步next,从而使得多个异步任务串行执行。
基于 Promise 的 Generator 流程控制
类似地,也可以使用Generator+Promise的方式:
js- const run = gen => {
- const g = gen()
- const next = data => {
- let result = g.next(data)
- if (result.done) return result.value
- result.value.then(next, err => {
- g.throw(err)
- })
- }
- next()
- }
然后这样子定义Generator函数:
js- const cat = (sound, name) => {
- return new Promise((resolve) => {
- window.setTimeout(() => {
- const data = {
- sound,
- name
- }
- resolve(data)
- }, 1000)
- })
- }
- function *gen () {
- const daijin = yield cat('miao', 'daijin')
- console.log(daijin)
- const sadaijin = yield cat('aowu', 'sadaijin')
- console.log(sadaijin)
- }
- run(gen)
总结
本文主要回顾了Generator实现异步流程控制的方法。看到这里,是不是觉得和async+await的语法很像,所以其实async+await的语法是一个Generator的语法糖。此外,在async+await出现以前,前端主要依赖 co 库来实现上述的流程控制,上面的代码相当于实现了一个 mini 版的 co。
