Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa源码分析系列(四)co-4.0新变化 #10

Open
purplebamboo opened this issue Jul 11, 2017 · 0 comments
Open

koa源码分析系列(四)co-4.0新变化 #10

purplebamboo opened this issue Jul 11, 2017 · 0 comments

Comments

@purplebamboo
Copy link
Owner

koa源码分析系列(四)co-4.0新变化

koa基于co实现,co又是使用了es6的generator特性,所以,没错这个特性支持很一般。
有下面几种办法体验generator:

  • node v0.11 可以使用 (node —harmony)
  • 使用gnode来使用,不过据说性能一般
  • 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用,重启chrome即可。

##核心代码分析

之前写过一篇co的源码分析文章,但是不久之后co就发生了重大变化,就是完全抛弃了thunk风格的函数。全部转用promise。于是,找了个时间我再次看了下源码。简单记录下。

本文假设你已经熟悉了es6里面promise的基本用法。如果不是特别清楚的可以参考下面几篇文章:

  1. http://purplebamboo.github.io/2015/01/16/promise/
  2. http://www.w3ctech.com/topic/721
  3. http://www.cnblogs.com/fsjohnhuang/p/4135149.html
  4. http://wohugb.gitbooks.io/ecmascript-6/content/docs/promise.html

co4.0全部采用promise来实现。下面我们分析下代码。

首先co的用法发生了改变:

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

可以看到co还是接受了一个generatorFunction作为参数,实际上参数如果是一个generator对象也是可以的。如果是generatorFunction,co内部会帮你执行生成对应的generator对象。

不同的是co不再返回一个thunk函数,而是返回了一个promise对象。

yield后面推荐的也是promise对象,而不是thunk函数了。

我们看下实现:

function co(gen) {
  var ctx = this;

  //如果是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.call(ctx, 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;
}

核心代码主要是onFulfilled与next的实现。

我们先不考虑错误处理看下执行流程。也先不看toPromise的实现。假定我们只是yield一个promise对象。

例子:

co(function* () {
  var a = yield Promise.resolve('传给a的值');
  var b = yield Promise.resolve('传给b的值');
  return b;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

假设:

  • Promise.resolve('传给a的值');生成的叫做promise对象A。
  • Promise.resolve('传给b的值');生成的叫做promise对象B。

onFulfilled作为入口函数。

  1. 调用gen.next(res)。这时候代码会执行到yield Promise.resolve('传给a的值');然后停住。拿到了返回值`{value:'promise对象A',done:false}。
  2. 然后调用next(ret),传递ret对象。next里面调用promise对象A的then添加操作函数。
  3. 等promise对象A变成了成功状态,就会再次调用onFulfilled,并且传入resolve的值。
  4. 于是再次重复1。代码会执行到yield Promise.resolve('传给b的值');停住。不同的是这次调用onFulfilled会传递res的值。通过gen.next(res)会把res也就是resolve的值赋值给a。

然后继续这个过程,一直到最后return的时候。

//co包裹的generatorFunction return后 ret.done为true。这个时候就可以resole `Co生成的promise对象`了。
if (ret.done) return resolve(ret.value);

这样整个调用链就执行下去了。可以看到主要是使用promise的then方法添加onfullied操作函数,来实现自动调用gen.next()

##co的错误处理

co的错误处理主要使用onRejected实现,基本逻辑跟onFulfilled差不多,这边主要说一下gen.throw(err);的原理。
generator对象的一个特性是可以在generatorFunction外面抛出异常,在generatorFunction里面捕获到这个异常。

function *test(){
    try{
        yield 'a'
        yield 'b'
    }catch(e){
        console.log('内部捕获:')
        console.log(e)
    }
}

var g = test()
g.next()

g.throw('外面报错消息')

/*结果
*内部捕获:
*外面报错消息
*
*/

当我们运行gen.next()的时候,会运行到yield 'a'这一句。这一句正好在内部的try范围内,因此g.throw('外面报错消息')这个抛出的错误会被捕获到。

如果我们不调用gen.next()或者连续调用三次gen.next()。代码执行不在try的范围,这个时候去gen.throw错误就不会被内部捕获到。

所以co里面用了这个特性,可以让你针对某一个或多个yield加上try,catch代码。
co发现某个内部promise报错就会调用onRejected然后调用gen.throw抛出错误。

如果你不处理错误,co就调用reject(err)传递给包装后的co返回的promise对象。这样你就可以在co(*fn).catch 拿到这个错误。

##toPromise的实现

我们看下toPromise的代码:

function toPromise(obj) {

  if (!obj) return obj;
  //是promise就直接返回
  if (isPromise(obj)) return obj;
  //如果是generator对象或者generatorFunction就直接用co包一层,最后会返回一个包装好的promise。
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  //如果是thunk函数就调用thunkToPromise转换
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  //是数组就使用arrayToPromise转换
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  //是对象就使用objectToPromise转换
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

主要就是各种判断,把不同类型的yield值转换成一个promise对象。
前面几个都很简单不说了。

thunkToPromise比较简单如下:

function thunkToPromise(fn) {
  var ctx = this;
  //主要就是新new一个promise对象,在thunk的回调里resolve这个promise对象
  return new Promise(function (resolve, reject) {
    fn.call(ctx, function (err, res) {
      //错误就调用reject抛出错误
      if (err) return reject(err);
      //对多个参数的支持
      if (arguments.length > 2) res = slice.call(arguments, 1);
      resolve(res);
    });
  });
}

arrayToPromise也比较容易:

function arrayToPromise(obj) {
  //直接调用Promise的静态方法包装一个新的promise对象。然后对于每个value调用toPromise进行递归的包装
  return Promise.all(obj.map(toPromise, this));
}

objectToPromise会稍微绕一点:

function objectToPromise(obj){
  //小技巧,生成一个跟obj一样类型的克隆空对象
  var results = new obj.constructor();
  //拿到 对象的所有key,返回key的集合数组
  var keys = Object.keys(obj);
  var promises = [];

  //遍历所有值
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];

    //递归调用
    var promise = toPromise.call(this, obj[key]);
    //如果转换后是promise对象,就异步的去赋值
    if (promise && isPromise(promise)) defer(promise, key);
    //如果不能转换,说明是纯粹的值。就直接赋值
    else results[key] = obj[key];
  }

  //监听所有队列里面的promise对象,等所有的promise对象成功了,代表都赋值完成了。就可以调用then,返回结果results了。
  return Promise.all(promises).then(function () {
    return results;
  });

  function defer(promise, key) {
    //先占位
    results[key] = undefined;
    //把当前promise加入待监听promise数组队列
    promises.push(promise.then(function (res) {
      //等当前promise变成成功态的时候赋值
      results[key] = res;
    }));
  }
}

objectToPromise的主要思路是循环递归遍历对象的值

  • 如果发现是纯粹的值,就直接赋值给结果对象。
  • 如果发现是可以转化为promise的就调用defer异步的把值添加到results里面,同时把promise对象放到监听的数组里。
  • 这样在最外围只要使用Promise.all去监听这些promise对象。等他们都执行完了代表results已经被正确的赋值。于是再通过then,改变要反回的promise对象的要resolve的值。

##结语

整个分析到这就结束了,新版的co代码非常清晰也更加容易理解。不过完全抛弃thunk不知道TJ大神怎么想的。好像目前的koa还是使用的老的co来实现的。不管怎么说,还是值得看一看的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant