co库的简易实现

generator

Generator 函数是 ES6 提供的一种异步编程解决方案。

function* Hello(){
 yield 1;
 var res = yield 2; // undefined 因为yield表达式本身没有返回值,或者说总是返回undefined。
}
var hello = Hello();
var r2 = hello.next() // { value:1, done:false }
console.log(hello.next());  // { value:1, done:false }
console.log(hello.next());  // {  value:2, done:false }
console.log(hello.next());  // { value:undefined, done:true }

执行generator函数会返回一个遍历器对象,一个指向内部状态的指针,可以依次遍历generator对象内部的每个状态。必须手动调用遍历器的next方法,函数才能从函数头部或者上一次停止的地方继续执行直到遇到下一个yield(或者return语句)。遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。就是调用next()返回的对象的value的值。

generator自执行

function delay(time){
 setTimeout(function(){
   // some code..
 },time);
}
function* YieldDelay(){
  yield delay(3200);
  console.log('3200ms done!');
  yield delay(4400);
  console.log('4400ms done!');
  yield delay(5500);
  console.log('5500ms done!');
}

上面这段代码我们必须手动的一直调用 YieldDelay的next才能执行完成。怎么可以自执行呢?

var g = YieldDelay()
var res = g.next();
while(!res.done){
  console.log(res.value);
  res = g.next();
}

上面这段代码。可以使得YieldDelay函数自动执行完。但是这不适合异步操作如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。
那么如果我们把g.next()当做参数传递到delay异步操作里当做回调函数的最后一步执行。是不是就可以了。

function delay(time,fn){
 setTimeout(function(){
     // some code..
    fn()
 },time);
}
function cl(){
  yieldDelay.next();
}

function* YieldDelay(){
  yield delay(3200,cl);
  console.log('3200ms done!');
  yield delay(4400,cl);
  console.log('4400ms done!');
  yield delay(5500,cl);
  console.log('5500ms done!');
}

var yieldDelay = YieldDelay();
yieldDelay.next();

但我们现在还需要手动的进行第一次next的调用。如何再进行优化。是的第一次next调用也可以自执行呢?上面代码delay函数有两个参数time和fn。time每个yield后面跟的时间都不一样。但是fn却都是相同的就是调用next()函数。那么我们再进行一层封装。使得delay可以不传递fn。

Thunk函数

thunk函数是一个偏函数,执行它会得到一个新的只带一个回调参数的函数。

function delay(time){
  return function(fn){
    setTimeout(function(){
      fn();
    },time)
  }
}

delay就是一个典型的Thunk函数。返回一个只有一个回调的新函数。co库就是基于Thunk函数的。以下是简易的实现。

function co(GenFunc) {
  return function(cb) {
    var gen = GenFunc();  // 第一次执行的时候构造出对象
    next()    // 调用自定义的next方法
    function next(err, res) {
      var ret = gen.next(res);   
     // 在generator函数中走一步,delay函数返回一个函数赋给ret.value
      if (ret.done) {    
        // 判断ret.done是否为真,如果为真,说明generator函数执行完了,该调用回调函数了
        cb && cb();
      } else {
      // 如果ret.done为假,那么调用上一个返回的函数,并且把next函数传递给它作为回调函数
        ret.value(next); // 如果是promise这里可以是 ret.value.then(next); 
      }
    }
  }
}

co库的使用

co(function* (){
  yield delay(4200);
  yield delay(4000);
  yield delay(3000);
})(function(){
  // 回调函数
  console.log('all done!');
})
yield跟array或对象

现在我们的co库要求yield后必须是thunk。我们进行拓展。如果yield跟的是array或者对象(但目前实现的是数组和对象的每个值都好说thunk)
我们就在it.value(next);前增加一句it.value = toThunk(it.value);
我们来看toThunk的实现

function isObject(obj){
  return obj && Object == obj.constructor;
}
function isArray(obj){
  return Array.isArray(obj);  // Array.isArray这个方法是ES5的,但并不支持所有的浏览器
}
function toThunk(obj,ctx){
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk(obj);
  }
  return obj;
}
function objectToThunk(obj){
  return function(done){
      var keys = Object.keys(obj);
      var results = new obj.constructor();
      var length = keys.length;
      var _run = function(fn,key){
        fn(function(err,res){// 这个参数就是thunk的回调。就是delay里的fn异步执行完执行的回调
          results[key] = res;// 数组或对象中的thunk执行结果
          --length || done(null, results); // 执行完一个数组长度减一
        })
      }
      foreach(var i in keys){ //执行数组或对象的每个thunk
        _run(Object[keys[i]],keys[i]);
      }
  }
}
Object.keys([1,2,3,4]) //[ '0', '1', '2', '3' ]
 Object.keys({"one":1,"two":2,"three":3}) //[ 'one', 'two', 'three' ]

new obj.constructor()这句,会根据obj的类型生成一个相关的空数组或者空对象。便于下面的赋值。objectToThunk也是一个thunk。因为最终objectToThunk才是it.value 。这里的done就相当于delay里的fn,也就是co库中的next函数。这样yield后跟的数组或对象里的thunk是并行执行的。直到所有的执行完成。
数组里面还是数组怎么办这可以使用递归嘛。跟深度遍历遇见数组里还是数组一样。
改变_run函数,增加三行代码

var _run = function(fn,key){
    //新增加判断是否数组里还是数组
    if(Array.isArray(fn)) {
        fn = toThunk(fn);
    }
    fn(function(err,res){
      results[key] = res;
      --length || done(null, results);
    })
}
yield跟promise

首先改写toThunk函数。增加promise的判断

function isPromise(obj) {
  return obj && 'function' == typeof obj.then;
}
function toThunk(obj){
  if (isObject(obj) || isArray(obj)) {
    return objectToThunk( obj);
  }
  if (isPromise(obj)) {
    return promiseToThunk(obj);
  }
  return obj;
}

objectToThunk函数的实现。同理还是把promise转换成thunk

function promiseToThunk(promise){
    return function(done){
        promise.then(function(err,res){
            done(err,res);
        },done)
    }
}

co的实现
co4.0完全抛弃了thunk风格的函数。全部转用promise。
4.0的用法

co(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

co的返回值不再是thunk函数了而是一个promise。并且也推荐yield后跟promise而不是thunk函数了。

function co(gen) {
  //如果是generatorFunction,就执行 获得对应的generator对象
  if (typeof gen === 'function') gen = gen.call(this);

  //返回一个promise
  return new Promise(function(resolve, reject) {

    //初始化入口函数,第一次调用
    onFulfilled();

    //成功状态下的回调
    function onFulfilled(res) {
      var ret;
      try {
        //拿到第一个yield返回的对象值ret
        ret = gen.next(res);
      } catch (e) {
        //出错直接调用reject把promise置为失败状态
        return reject(e);
      }
      //开启调用链
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        //抛出错误,这边使用generator对象throw。这个的好处是可以在co的generatorFunction里面使用try捕获到这个异常。
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }


    function next(ret) {
      //如果执行完成,直接调用resolve把promise置为成功状态
      if (ret.done) return resolve(ret.value);
      //把yield的值转换成promise
      //支持 promise,generator,generatorFunction,array,object
      //toPromise的实现可以先不管,只要知道是转换成promise就行了
      var value = toPromise(ret.value);

      //成功转换就可以直接给新的promise添加onFulfilled, onRejected。当新的promise状态变成结束态(成功或失败)。就会调用对应的回调。整个next链路就执行下去了。
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);

      //否则说明有错误,调用onRejected给出错误提示
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

function isPromise(obj) {
  return 'function' == typeof obj.then;
}

thunk是给异步成功回调里传参执行next。promise是把next在then的成功回调里执行。
promise版的co库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容

  • 异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本...
    呼呼哥阅读 7,308评论 5 22
  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 484评论 0 0
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,710评论 0 5
  • 本文为阮一峰大神的《ECMAScript 6 入门》的个人版提纯! babel babel负责将JS高级语法转义,...
    Devildi已被占用阅读 1,981评论 0 4
  • 本文首发在个人博客:http://muyunyun.cn/posts/7b9fdc87/ 提到 Node.js, ...
    牧云云阅读 1,684评论 0 3