ES6 Generator 的异步使用技巧

分享

异步

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

  • callback 回调
  • Promise
  • event 事件

简单说说以上 3 种模式:

回调

将需要执行的函数传入异步调用内,在异步调用结束后,主动执行传入的函数。
function doSomeAsync(callback) {
    let data = {/* 数据 */};
    setTimeout(() => {
        callback(data);
    });
}

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

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

Promise

出现了回调地狱,我们得解决,这就出现了 Promise,将上面回调的例子改写成 Promise 的形式。

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

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

虽然这样写代码上看起来和回调的形式差不多,但如果在一个异步操作之后还要继续进行异步操作的话,在调用一次 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 参数的使用有疑问,请参考 Generator 中的 next

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

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

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

这是同步模式下 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 来说,更想要的应该是异步结束后的结果,那我们调整一下自动执行函数的内部逻辑,使得 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 方法,区别只是同步模式下用循环,异步模式下是递归而已。

我们实现了基于 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;
}

不同于 Promise 的代码,genCallback 生成的 Generator 函数调用 next 返回的 value 是一个需要执行的函数,函数执行后会进行真正的异步调用。

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

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

至此,我们基本上实现了对异步形式的控制,不管是如何实现自动化的,其 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 仅仅不过是一层语法糖罢了,其本质是 Promise 的使用,在 JavaScript 中,是不可能实现正真的同步,设计如此,而这些语法的实现,不得不说思维的伟大。

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。 原因如下: 1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。 什么是对象? 既然要说清楚,

By Breeze

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze