Generator函数异步应用

转载请注明出处: Generator函数异步应用

上一篇文章详细的介绍了Generator函数的语法,这篇文章来说一下如何使用Generator函数来实现异步编程。

或许用Generator函数来实现异步会很少见,因为ECMAScript 2016的async函数对Generator函数的流程控制做了一层封装,使得异步方案使用更加方便。

但是呢,我个人认为学习async函数之前,有必要了解一下Generator如何实现异步,这样对于async函数的学习或许能给予一些帮助。

文章目录

  1. 知识点简单回顾
  2. 异步任务的封装
  3. thunk函数实现流程控制
  4. Generator函数的自动流程控制
  5. co模块的自动流程控制

知识点简单回顾

在Generator函数语法解析篇的文章中有说到,Generator函数可以定义多个内部状态,同时也是遍历器对象生成函数。yield表达式可以定义多个内部状态,同时还具有暂停函数执行的功能。调用Generator函数的时候,不会立即执行,而是返回遍历器对象。

遍历器对象的原型对象上具有next方法,可以通过next方法恢复函数的执行。每次调用next方法,都会在遇到yield表达式时停下来,再次调用的时候,会在停下的位置继续执行。调用next方法会返回具有value和done属性的对象,value属性表示当前的内部状态,可能的值有yield表达式后面的值、return语句后面的值和undefined;done属性表示遍历是否结束。

yield表达式默认是没有返回值的,或者说,返回值为undefined。因此,想要获得yield表达式的返回值,就需要给next方法传递参数。next方法的参数表示上一个yield表达式的返回值。因此在调用第一个next方法时可以不传递参数(即使传递参数也不会起作用),此时表示启动遍历器对象。所以next方法会比yield表达式的使用要多一次。

更加详细的语法可以参考这篇文章。传送门:Generator函数语法解析

异步任务的封装

yield表达式可以暂停函数执行,next方法可以恢复函数执行。这使得Generator函数非常适合将异步任务同步化。接下来会使用setTimeout来模拟异步任务。

const person = sex => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        sex,
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)
}
const g = gen()
const next1 = g.next() // {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})

从上面代码可以看出,第一次调用next方法时,启动了遍历器对象,此时返回了包含value和done属性的对象,由于value属性值是promise对象,因此可以使用then方法获取到resolve传递过来的值,再使用带有data参数的next方法给上一个yield表达式传递返回值。

此时在const data = yield person()这句语句中,就可以得到异步任务传递的参数值了,实现了异步任务的同步化。

但是上面的代码会有问题。每次获取异步的值时,都要手动执行以下步骤

const g = gen()
const next1 = g.next() {value: Promise, done: false}
next1.value.then(data => {
  g.next(data)
})

上面的代码实质上就是每次都会重复使用value属性值和next方法,所以每次使用Generator实现异步都会涉及到流程控制的问题。每次都手动实现流程控制会显得麻烦,有没有什么办法可以实现自动流程控制呢?实际上是有的: )

thunk函数实现流程控制

thunk函数实际上有些类似于JavaScript函数柯里化,会将某个函数作为参数传递到另一个函数中,然后通过闭包的方式为参数(函数)传递参数进而实现求值。

函数柯里化实现的过程如下

function curry (fn) {
  const args1 = Array.prototype.slice.call(arguments, 1)
  return function () {
    const args2 = Array.from(arguments)
    const arr = args1.concat(args2)
    return fn.apply(this, arr)
  }
}

使用curry函数来举一个例子: )

// 需要柯里化的sum函数
const sum = (a, b) => {
  return a + b
}
curry(sum, 1)(2)   // 3

而thunk函数简单的实现思路如下:

// ES5实现
const thunk = fn => {
  return function () {
    const args = Array.from(arguments)
    return function (callback) {
      args.push(callback)
      return fn.apply(this, args)
    }
  }
}

// ES6实现
const thunk = fn => {
  return function (...args) {
    return function (callback) {
      return fn.call(this, ...args, callback)
    }
  }
}

从上面thunk函数中,会发现,thunk函数比函数curry化多用了一层闭包来封装函数作用域。

使用上面的thunk函数,可以生成fs.readFile的thunk函数。

const fs = require('fs')
const readFileThunk = thunk(fs.readFile)
readFileThunk(fileA)(callback)

使用thunk函数将fs.readFile包装成readFileThunk函数,然后在通过fileA传入文件路径,callback参数则为fs.readFile的回调函数。

当然,还有一个thunk函数的升级版本thunkify函数,可以使得回调函数只执行一次。原理和上面的thunk函数非常像,只不过多了一个flag参数用于限制回调函数的执行次数。下面我对thunkify函数做了一些修改。源码地址: node-thunkify

const thunkify = fn => {
  return function () {
    const args = Array.from(arguments)
    return function (callback) {
      let called = false
      // called变量限制callback的执行次数
      args.push(function () {
        if (called) return
        called = true
        callback.apply(this, arguments)
      })
      try {
        fn.apply(this, args)
      } catch (err) {
        callback(err)
      }
    }
  }
}

举个例子看看: )

function sum (a, b, callback) {
  const total = a + b
  console.log(total)
  console.log(total)
}

// 如果使用thunkify函数
const sumThunkify = thunkify(sum)
sumThunkify(1, 2)(console.log)
// 打印出3

// 如果使用thunk函数
const sumThunk = thunk(sum)
sumThunk(1, 2)(console.log)
// 打印出 3, 3

再来看一个使用setTimeout模拟异步并且使用thunkify模块来完成异步任务同步化的例子。

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
const g = gen()
const next = g.next()
next.value(data => {
  g.next(data)
})

从上面代码可以看出,value属性实际上就是thunkify函数的回调函数(也是person的第二个参数),而'boy'则是person的第一个参数。

Generator函数的自动流程控制

在上面的代码中,我们可以将调用遍历器对象生成函数,返回遍历器和手动执行next方法以恢复函数执行的过程封装起来。

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value(next)
  }
  next()
}

使用run函数封装起来之后,run内部的next函数实际上就是thunk(thunkify)函数的回调函数了。因此,调用run即可实现Generator的自动流程控制。

const person = (sex, fn) => {
  window.setTimeout(() => {
    const data = {
      sex,
      name: 'keith',
      height: 180
    }
    fn(data)
  }, 1000)
}
const personThunk = thunkify(person)
function *gen () {
  const data = yield personThunk('boy')
  console.log(data)
}
run(gen)
// {sex: 'boy', name: 'keith', height: 180}

有了这个执行器,执行Generator函数就方便多了。不管内部有多少个异步操作,直接把Generator函数传入run函数即可。当然,前提是每一个异步操作,都要是thunk(thunkify)函数。也就是说,跟在yield表达式后面的必须是thunk(thunkify)函数。

const gen = function *gen () {
  const f1 = yield personThunk('boy') // 跟在yield表达式后面的异步行为必须使用thunk(thunkify)函数封装
  const f2 = yield personThunk('boy')
  // ...
  const fn = yield personThunk('boy')
}
run(gen)  // run函数的自动流程控制

上面代码中,函数gen封装了n个异步行为,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。

co模块的自动流程控制

在上面的例子说过,表达式后面的值必须是thunk(thunkify)函数,这样才能实现Generator函数的自动流程控制。thunk函数的实现是基于回调函数的,而co模块则更进一步,可以兼容thunk函数和Promise对象。先来看看co模块的基本用法

const co = require('co')
const gen = function *gen () {
  const f1 = yield person('boy') // 调用person,返回一个promise对象
  const f2 = yield person('boy')
}
co(gen)   // 将thunk(thunkify)函数和run函数封装成了co模块,yield表达式后面可以是thunk(thunkify)函数或者Promise对象

co模块可以不用编写Generator函数的执行器,因为它已经封装好了。将Generator函数co模块中,函数就会自动执行。

co函数返回一个Promise对象,因此可以用then方法添加回调函数。

co(gen).then(function (){
  console.log('Generator 函数执行完成')
})

co模块原理;co模块其实就是将两种自动执行器(thunk(thunkify)函数和Promise对象),包装成一个模块。使用co模块的前提条件是,Generator函数的yield表达式后面,只能是thunk(thunkify)或者Promise对象,如果是数组或对象的成员全部都是promise对象,也可以使用co模块。

基于Promise对象的自动执行

还是使用上面例子,不过这次是将回调函数改成Promise对象来实现自动流程控制。

const person = (sex, fn) => {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      const data = {
        name: 'keith',
        height: 180
      }
      resolve(data)
    }, 1000)
  })
}
function *gen () {
  const data = yield person('boy')
  console.log(data)   // {name: 'keith', height: 180}
}
const g = gen()
g.next().value.then(data => {
  g.next(data)
})

手动执行实际上就是层层使用then方法和next方法。根据这个可以写出自动执行器。

const run = gen => {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value.then(data => {
      next(data)
    })
  }
  next()
}
run(gen)  // {name: 'keith', height: 180}

如果对co模块感兴趣的朋友,可以阅读一下它的源码。传送门:co

关于Generator异步应用的相关知识也就差不多了,现在稍微总结一下。

  1. 由于yield表达式可以暂停执行,next方法可以恢复执行,这使得Generator函数很适合用来将异步任务同步化。
  2. 但是Generator函数的流程控制会稍显麻烦,因为每次都需要手动执行next方法来恢复函数执行,并且向next方法传递参数以输出上一个yiled表达式的返回值。
  3. 于是就有了thunk(thunkify)函数和co模块来实现Generator函数的自动流程控制。
  4. 通过thunk(thunkify)函数分离参数,以闭包的形式将参数逐一传入,再通过apply或者call方法调用,然后配合使用run函数可以做到自动流程控制。
  5. 通过co模块,实际上就是将run函数和thunk(thunkify)函数进行了封装,并且yield表达式同时支持thunk(thunkify)函数和Promise对象两种形式,使得自动流程控制更加的方便。

参考资料

  1. Generator 函数的异步应用
  2. node-thunkify
  3. co
posted @ 2018-01-23 22:03  凯斯keith  阅读(1787)  评论(0编辑  收藏  举报