此博客不再维护,博客已迁移至 https://github.com/purplebamboo/blog/issues
文章目录
  1. 1. 核心代码分析
  2. 2. co的错误处理
  3. 3. toPromise的实现
  4. 4. 结语

koa是TJ大神新一代的中间件框架,本系列旨在一步一步实现koa的功能,包括下面这些。

  1. koa源码分析系列(一)generator
  2. koa源码分析系列(二)co的实现
  3. koa源码分析系列(三)koa的中间件机制实现
  4. 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的用法发生了改变:

1
2
3
4
5
6
7
8
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函数了。

我们看下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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对象。

例子:

1
2
3
4
5
6
7
8
9
10
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的时候。

1
2
//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里面捕获到这个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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比较简单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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也比较容易:

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

objectToPromise会稍微绕一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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来实现的。不管怎么说,还是值得看一看的。

文章目录
  1. 1. 核心代码分析
  2. 2. co的错误处理
  3. 3. toPromise的实现
  4. 4. 结语