Generator函数和异步操作

在这一部分,我们会写一个辅助函数来展示如何利用Generator函数完成异步操作。在这之前,我们先复习一下,把参数传递给g.next会发生些什么。如果你还记得前面的内容——如果你传递一个参数给g.next,该参数的值将会替换上次yield生成的值:

function* myGenerator(n) {
  const a = (yield 10) + n
  yield a
}
const g = myGenerator(1)
g.next().value // --> 10
g.next(100).value // --> 101

我们将在辅助函数中充分利用这一特性,现在,我们先写一个返回promise对象的异步函数:

const asynTask1 = () => new Promise((r, j) => setTimeout(() => r(1), 1000))

这个函数返回一个promise对象,并在1秒钟以后resolve一个值1。现在,我们创建一个Generator函数,并在它的内部调用我们的异步函数:

const asynTask1 = () => new Promise((r, j) => setTimeout(() => r(1), 1000))
function* main() {
  const result = yield asynTask1()
}
const g = main()
console.log(g.next())

猜想一下,上面的代码会输出什么呢?让我们来分析一下大概的执行流程:

  • 首先,我们调用了Generator函数main,并把返回的generator对象赋值给变量g
  • 然后,调用了next方法,来获取一个yield表达式的值,也就是asyncTask1()的值,它返回了一个promise对象。
  • 最后,我们把结果打印了出来:{value: Promise { <pending> }, done: false}
  • 1秒之后,这段代码执行完毕

代码执行完毕以后,我们无法通过generator对象获取到resolve的值。除非,我们再次调用g.next方法,并且在“合适”的时间把resolve的值作为参数传入进去,在这个例子中,也就是使用resolve的值来替换yield asyncTask1。下面,我们更新一下上面的代码:

const asynTask1 = () => new Promise((r, j) => setTimeout(() => r(1), 1000))

function* main() {
  const result = yield asynTask1()
  return result //<-- return the resolved value and mark the end.
}

const g = main()
const next = g.next()
console.log(next) // --> { value: Promise { <pending> }, done: false }

next.value.then(v => {
  // Resolve promise.
  const r = g.next(v) // passing the resolved value to next.
  console.log(r) // -> { value: 1, done: true }
})

我们在Generator函数中只是简单地增加了return语句,并返回了resolve的值。重要的是,当我们resolve promise对象的时候,我们把resolve的值传递给了g.next

接下来,我们就可以编写辅助函数了,这个辅助函数会接受一个Generator函数,并且完成上面我们讨论过的内容。如果没有需要再生成的值,它将会返回resolve的值。首先,我们这样定义辅助函数:

const helper = gen => {
  const g = gen()
}

目前来看,没有什么特别的,我们把Generator函数作为参数传递到了辅助函数中,并在其内部调用Generator函数,将结果赋值给了变量g。下一步,我们需要定义一个函数来为我们调用g.next方法:

const helper = gen => {
  const g = gen()
  function callNext(resolved) {
    const next = g.next(resolved) // replace the last yield with the resolved value
    if (next.done) return next.value // return the resolved value if not more items
    return next.value.then(callNext) // pass `callNext` back again.
  }
}

callNext函数只接受一个参数:promise对象的resolve值,在其内部我们使用resolve值调用g.next方法,如果异步操作完成,那么generator对象中就会包含resolve值;如果没有完成,那么我们就把callNext方法作为then方法的参数进行递归调用。如此,我们会一直调用g.next方法获取生成的值,直到生成完所有的值为止。现在,我们可以使用辅助函数了,我们只需简单的调用它,并把Generator函数传递给它:

helper(function* main() {
  const a = yield asynTask1()
  console.log(a)
})

运行上面的代码,发现没有打印任何结果,这是因为我们还缺少了一个动作。我们没有调用callNext函数,为此,我们需要让它自调用:

const helper = gen => {
  const g = gen()
  ;(function callNext(resolved) {
    const next = g.next(resolved)
    if (next.done) return next.value
    return next.value.then(callNext)
  })() // <-- self invoking
}

现在,我们的辅助函数基本完成了,我们再加上一些错误捕获代码:

const helper = gen => {
  const g = gen()
  ;(function callNext(resolved) {
    const next = g.next(resolved)
    if (next.done) return next.value
    return next.value.then(callNext).catch(err => g.throw(err)) // <-- throw error
  })()
}

如果异步任务出现任何异常,generator对象将抛出一个错误,此时我们可以直接在Generator函数中使用try-catch语句来处理错误。所有的代码整合起来如下所示:

code/generators/async-flow.js

const asynTask1 = () => new Promise(r => setTimeout(() => r(1), 1000))
const asynTask2 = () =>
  new Promise((r, j) => setTimeout(() => j(new Error('e')), 500))
const helper = gen => {
  const g = gen()
  ;(function callNext(resolved) {
    const next = g.next(resolved)
    if (next.done) return next.value
    return next.value.then(callNext).catch(err => g.throw(err))
  })()
}
helper(function* main() {
  try {
    const a = yield asynTask1()
    const b = yield asynTask2()
    console.log(a, b)
  } catch (e) {
    console.log('error happened', e)
  }
})

如果你对此比较感兴趣,你可以看看co库,那里有更全面的实现。我们将在下一章讨论async-await,它们是基于Generator和Promise的高层抽象,用来解决异步问题。

用户头像
登录后发表评论