Analysis of KOA (2)

Time:2021-8-20

Flow control of KOA

In the previous article, the principle of Co3. X version was analyzed. Since CO is implemented from 4.0 using the standard promise of ES6, the following is a brief introduction:

I need you to be rightpromiseHave a certain understanding)

Here, the return object of yield changes from the thunk method to the promise object. Since they all accept methods as parameters, the generator can control and continue to iterate through this callback method. Here is a sample code:

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

  var next = function (res) {
    //Now res.value is promise, and other data types are packaged as promise within Co
    res.value.then(function (res) { 
      next(gen.next(res));
    }, function () {
      gen.throw(err);
    });
  };

  next(gen.next());
}

Here, each time the object returned by the generator becomes promise, and then we recursively call the next method in the resolve method of promise, so that the generator can continue to iterate(termination judgment and exception handling are not added here)

In previous versions, many thunk packaging methods were used. In order to maintain compatibility, the co made a judgment on this. If res.value is not promise but thunk, it will be compatible. For this, we do not need to modify the previous code. Here is an example:

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

I won’t introduce the encapsulation of other data types supported by Co. if you are interested, you can see the source code of Co.


Well, after a long talk, it’s time to talk about KOA. We usually add a genfunc through use when using koa, so let’s see what use does first:

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

It does nothing but save parameters and return references to support chained calls. Next, let’s look at the startup of KOA:

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

This is very similar to the official HTTP demonstration of node. There is nothing to explain. Let’s look at the callback method:

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);
  }
};

Here, before calling Co, it uses the compose method to process the callback we registered earlier. Compose is the method in the koa compose package. This is the source code:

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;
  }
}

This is the core of KOA control flow. In fact, the code is very simple, but it needs careful analysis

1. The while loop is in reverse order, that is, the parameter received by the last genfunc is NOOP, and the source code is:

function *noop(){}

2. Starting from the penultimate genfunc, the parameters accepted by each method are the instance of the next genfunc, that is, the formal parameter we refer to in the koa method: next, so that the flow process can be controlled by using yield next in the method.

3. The compose method also returns a generator, so that CO can iterate over it, but the data it generates each time is not promise, but an instance of our registered genfunc. Therefore, it uses yield * prev to transfer the iteration object, so that we register the generator for each iteration.

4. If you see here and understand it, you will have a question: why do we use yield next instead of yield * next in our registered generator. In fact, you can write whatever you want. The type of res.value is judged internally by Co. if it is a generator, it will recursively call CO (generator), and co will return a promise( The core method I wrote does not describe this)

Here is the difference between yield generator and yield * generator: the former takes a generator as the return value, while the latter gives control to the generator and iterates it. See the detailsMDN-function *


After writing here, the implementation method and flow control of KOA are basically clear. Here is a summary:

1. In the gefunc registered by koa, you can control the flow of the program through yield next. If you accidentally forget to do this, you can find through the above code that the program will abandon the following generator and return in advance.

2. The whole control flow is actually like an onion. The first one will come out later, and the last one will come out first. However, if we deliberately skip the next call of a step, the onion will not pass through the middle, but through some outer layers. Then, when we pass through these outer layers in reverse order, the program will be executed and will not enter the genfunc registered later.

3. For the above reasons, there is a trap:If you use components like router, generally speaking, if a specified path is matched, the router will execute, and then the genfunc behind it will be discarded and returned in advance. If you want to configure some global processing, such as compressing HTML and closing database connections, which must be executed last, you must register before these methods that may interrupt the control flow, and then transfer them directly to the next step through yield next. However, they may not be returned in advance and not executed.

After finishing work, criticism and correction are welcome.