3

KOA的流转控制

上一篇分析了co3.x版本的原理,由于co从4.0采用es6的标准promise来实现,简要介绍下:

( 需要你对 promise 有一定的了解 )

在这里,yield的返回对象从thunk方法变成promise对象,由于它们都接受方法作为参数,这样generator便能通过这个回调方法来控制,持续迭代下去,这里给个示范代码:

function core(genfunc) {
  var g = genfunc();

  var next = function (res) {
    // 现在res.value是promise,co内部对它其他数据类型包装成promise
    res.value.then(function (res) { 
      next(gen.next(res));
    }, function () {
      gen.throw(err);
    });
  };

  next(gen.next());
}

这里,每次生成器返回的对象变成了promise,然后我们在promise的resolve方法中递归调用next方法,这样生成器就可以持续迭代下去了. (这里没加终止判断和异常处理)

由于之前的版本中,有很多使用了thunk的包装方法,为了保持兼容,co中对此做了判断,如果res.value不是promise,而是thunk,它会做兼容处理,对此我们不用修改之前的代码,这里是示范:

function thunkToPromise(fn) {
  return new Promise(function (resolve, reject) {
    fn(function(err, res) {
      resolve(res);
    });
  });
}

对于co支持的其他数据类型的封装,我就不介绍了,感兴趣的可以去看co的源码。


好了,长篇大论铺垫了这么久,该说说koa了,我们通常使用koa的时候都是通过use添加一个genfunc,所以先看看use做了什么:

app.use = function(fn){
  this.middleware.push(fn);
  return this;
};

它什么也没做,只是将参数保存起来,然后返回引用,以便支持链式调用,接下来我们看看koa的启动:

app.listen = function(){
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

这个和node的官方http示范很像,没什么要解释的,再看看callback方法:

app.callback = function(){
  var mw = [respond].concat(this.middleware);
  var gen = compose(mw);
  var fn = co.wrap(gen);
  var self = this;

  if (!this.listeners('error').length) this.on('error', this.onerror);

  return function(req, res){
    res.statusCode = 404;
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn.call(ctx).catch(ctx.onerror);
  }
};

这里,在调用co之前,它采用compose方法对之前我们注册的回调做了一次处理,compose是koa-compose包中的方法,这是源码:

function compose(middleware){
  return function *(next){
    var i = middleware.length;
    var prev = next || noop();
    var curr;

    while (i--) {
      curr = middleware[i];
      prev = curr.call(this, prev);
    }

    yield *prev;
  }
}

这里是koa控制流的核心,其实代码很简单,但是需要细心分析下

1.while循环是逆序的,也就是说最后一个genfunc接收的参数是noop, 源码是:

function *noop(){}

2.从倒数第二个genfunc开始,每一个方法接受的参数都是紧挨它下一个genfunc的实例,也就是我们在koa方法中引用的形参: next,这样便能通过在方法中使用yield next来控制流转过程。

3.compose方法返回的也是一个generator,这样co便能迭代它,但是它每次产生的数据并不是promise,而是我们注册的genfunc的实例,所以这里它使用了yield *prev,用来转移迭代对象,这样每次迭代的就是我们注册是生成器了。

4.如果你看到这里你看懂了,就会有个疑问:为什么在我们注册的生成器中,我们使用的是yield next,而不是yield *next。其实你怎么写都可以,co内部对res.value做了类型判断,如果是generator,它自己会递归调用co(generator),而co也会返回一个promise。(我写的core方法没有对此进行描述)

这里说明下 yield generator 和yield *generator的区别:前者会将一个generator作为返回值,后者则会将控制交给这个generator,并迭代它,详细介绍可以看 MDN-function *


写到这里,koa的实现方法和流转控制基本就清楚了,这里做一个总结:

1.在koa注册的genfunc中,可以通过yield next 来控制程序的流转,如果不小心忘记了做这个事情,那么通过上面的代码可以发现,程序会舍弃后面的generator,提前返回。

2.整个控制流实际像一个洋葱,先进的一定会后出,后进入的一定会先出,但是如果我们故意跳开某一步的next调用,那么这个洋葱就不会被从中间穿过,而是穿过部分外层,然后逆序在穿过这些外层,程序就执行完了,不会进入后面注册的那些genfunc。

3.由于上面的原因,这里有一个陷阱:如果你使用了类似router之类的组件,通常来说一般如果匹配了某个指定的path,这个router就会执行,然后它后面的genfunc会被舍弃,它会提前返回。而如果你要配置一些全局的处理,譬如压缩html,关闭数据库连接这类必须等到最后执行的操作,则必须在这些有可能会中断控制流的方法之前注册,然后通过yield next直接转移给下一步即可,不然有可能会被提前返回不被执行。

打完收工,欢迎批评指正。


whosesmile
2k 声望21 粉丝

粗通NODE,精通JS,熟悉Angular,React,Vue等前端框架。


引用和评论

0 条评论