/ ECMAScript6

ECMAScript 之 Generator 的异步使用技巧

标签: ECMAScript 6 javaScript

异步

js 界,异步是获取数据的主旋律,大概有以下几种

  • callback 回调
  • Promise
  • event 事件

简单的回顾一下以上 3 种模式:

回调

简单的来说就是将需要执行的函数传入异步调用内,在异步调用结束后,执行函数,写出的代码会是这样:

function doSomeAsync(callback) {
    let data = {/* 数据 */}
    setTimeout(() => {
        callback(data)
    })
}

doSomeAsync((data) => {
    // do some thing
})

但试想一下,如果说回调之中还有回调,那么将会形成一个类似金字塔的结构(回调地狱),代码将变的非常难以维护。

Promise

出现了回调地狱,我们得解决,这就出现了 PromisePromise 的理解,参考《ES6 入门》中关于 Promise 的章节,我们把上面回调的例子改写成 Promise 的形式。

let promise = new Promise((resolve, reject) => {
    let data = {/* 数据 */}
    setTimeout(() => {
        resolve()
    })
})

promise.then(res => {
    // do some thing
})

虽然这样写代码上看起来和回调的形式差不多,但如果在一个异步操作之后还要继续进行异步操作的话,在调用一次 then 方法即可,具体细节可以在上面给的链接中查看。

事件

事件实现了 发布/订阅 模式,具体的过程如下:

  1. 在异步操作结束前,定义操作结束后的事件名以及对应的事件处理函数
  2. 异步操作结束时,触发相应事件

关于事件类该如何实现,可以参考我自己实现的一个事件类 Event

ok 把上面的例子改一改:

let event = new Event()
event.$on('asyncEnd', () => {
    // do some thing
})

setTimeout(() => {
    // do some thing
    event.$emit('asyncEnd')
})

ok 主流的异步解决方式都已经说完,我们来看看 Generator 函数在异步中的应用。

Generator 的异步使用

先来个 Generator 函数

function* gen(x) {
  let y = yield x + 2
  console.log(y)
  let z = yield y + 3
  console.log(z)
  return z
}

然后调用

let g = gen(1)
gen.next()   // {value: 3, done: false}
gen.next(3)  // {value: 6, done: false}
gen.next(6)  // {value: 6, done: true}

注意: next 中传的参数即为上一次调用 next 返回值中的 value 也就是 yield 表达式的返回值。如果对于 next 参数的使用有疑问,请参考《ES6 入门》中关于 Generator 的章节。

接下来,我们来写一个函数让 next 方法自动执行

function autoGen(gen){
    let genReturn = {
        value: undefined, 
        done: false
    }
    do {
        genReturn = gen.next(genReturn.value)
    } while(!genReturn.done)
}

autoGen(gen(1))
// 3
// 6

ok 这是同步模式下 Generator 函数的基本使用方式,那我们现在来改一改,将 yield 后的表达式改为一个异步调用( Promise )。

function createPromise(num){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(num)
        })
    })
}

function* genPromise(x) {
  let y = yield createPromise(x + 2)
  console.log(y)
  let z = yield createPromise(y + 3)
  console.log(z)
  return z
}

如果我们按照之前的调用形式,得到的 value 将会是一个 Promise 而且对于变量 yz 来说,更想要的应该是异步结束后的结果,ok 我们调整一下自动执行函数的内部逻辑,使得 next 方法中的参数是异步结束后的结果。

function autoGenForPromise(gen){
    // 获得第一个 promise
    let promiseChain = gen.next().value
    
    function next(){
        // promise.then 能拿到异步的结果
        // 所以将 next 方法放在 then 方法内执行
        promiseChain.then(res => {
            let innerReturn = gen.next(res)
            // 判断 Generator 函数是否已经执行完毕
            if(!innerReturn.done){
                // 获取下一个 promise
                promiseChain = innerReturn.value
                // 递归调用
                next()
            }
        })
    }
    
    next()
}

autoGenForPromise(genPromise(1))
// 3
// 6

同样的只是在不断的调用 next 方法,区别只是同步模式下用循环,异步模式下是递归而已。

ok 我们实现了基于 PromiseGenerator 函数的自动化,那么该考虑考虑,基于回调(callback)的自动化了。

具体如何实现的过程不做过多的解读,主要看代码

// 参数和回调分两个步骤传入异步中
function createCallback(num){
    return function(callback){
        setTimeout(() => {
            callback(num)
        })
    }
}

function* genCallback(x) {
  let y = yield createCallback(x + 2)
  console.log(y)
  let z = yield createCallback(y + 3)
  console.log(z)
  return z
}

不同于以上的代码,genCallback 生成的 Generator 函数调用 next 返回的 value 是一个需要执行的函数,函数执行后会进行真正的异步调用,这也是不同于 Promise 的一点。至于为什么是这样形式,这里不做过多的深入。

接着我们来让它实现异步自动化执行

function autoGenForCallback(gen){
    // 获取第一个 callback
    let nextResutl = gen.next()
    
    function next(){
        // 返回的 value 允许我们传入一个函数
        nextResutl.value((res) => {
           nextResutl = gen.next(res)
            // 判断是否结束
           if(!nextResutl.done){
                next()
            }
        })
    }
    
    next()
}

autoGenForCallback(genCallback(1))
// 3
// 6

ok 至此,我们基本上实现了对异步形式的控制,不管是如何实现自动化的,其 Generator 函数大致是不会变的,都是如下的格式:

function* gen(x) {
  let y = yield // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(y)
  let z = yield // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(z)
  return z
}

而我们之所以将不同的形式拆成不同的方法,其实也是为了方便理解,想象一下,如果我们在自动化函数中做了对不同形式的兼容,不就成了一个完美的 Generator 函数异步自动执行的方案了?

至于这个方案的具体代码,可以查看 npm 上一个叫 co 的模块,感兴趣的可以去了解下。

总结

有了 Generator 函数加上一个良好的自动化执行函数后,我们就可以像写同步代码那样写异步代码了,大体的结构就是上面说的那个样子。

function* gen(x) {
  let y = yield // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(y)
  let z = yield // 一个表达式
  
  // 对于上一个 yield 的返回值进行的操作
  console.log(z)
  return z
}
autoGen(gen(/* initValue */))

其实这段代码很像 ES7 中的 async/await ,只不过这里是 */yield/autoGen ,所以其实在 ES7async/await 仅仅不过是一层语法糖罢了,在 javascript 下,是不可能实现正真的同步,设计如此,而这些语法的实现,不得不说思维的伟大。