sensai2.0

308
15

序. Koa框架用了有一段时间了,虽然业务代码写的飞起,但是对Koa​实现原理还很陌生。所以特地抽了一段时间学习了一下Koa v1.0的源码,了解了co和generator的思想。

虽然现在es7已经支持了原生的async/await,并且基于generator的koa1已经落后了,但是其背后的思想以及巧妙的co模块依然值得学习。

1. koa与express传统框架之间的对比

之前一篇文章提过了express内部实现的机制原理,express core维护了一个Array,中间每个值都是一个中间件方法,中间件方法支持传入next使得core能够不断递归运行中间件,实际上这背后的原理与Promise是相通的。

express中间件模式解决了异步回调的问题,但是写在中间件内部的业务方法却依然需要面临回调地狱的问题。

如下面的例子:

app.use('/', (req, res, next) => {
  let name = req.query.name;
  fs.readFile(name, 'ascii', (err, data) => {
     name = data;
     fs.readFile(name, 'ascii', (err, data) => {
       name = data;
       fs.readFile(name, 'ascii', (err, data) => {
         name = data;
         fs.readFile(name, 'ascii', (err, data) => {
           res.body = data;
           next();
         });
       });
    });
  });
});
​相比之下,使用koa 1.0来实现上面的代码就显得优雅许多了:

const q = require('q');
app.use('/', function *(req, res, next) {
  const readFile = q.nfbind(fs.readFile);
  let name = req.query.name;
  for (let i = 0; i < 4; i++) {
    name = yield readFile(name, 'ascii');
  }
  yield next();
});

可以看到,koa1.0巧妙的通过generator这个es6 feature将异步代码变成了同步的写法,这样一来对业务开发人员就显得无比的友好。


2. co模块与generator的自动执行

要理解koa的内部原理,需要先知道es6中的new feature生成器generator

generator的概念网上一搜到处都有,懒得多扯~其实“生成器”这个特性在python、php中都有,generator实际上类似于一个迭代器iterator,而generator function中每个yield相当于iterator返回下一个值。

可以用for...of的语法来迭代一个generator,也可以直接调用generator的next方法来手动迭代之,示例如下:

// 定义一个generator
function *list() {
  for (let i = 0; i < 100; i++) {
    yield i;
  }
}
// 用for...of来迭代之
for (var x of list()) {
  console.log(x); // output 0, 1, 2, ... 99
}
// 用generator.next()迭代之
const i = list();
let j;
do {
  j = i.next();
  console.log(j.value);
} while (!j.done);

可以看出,generator的优势是可以在不生成列表本身的同时​定义一个列表的生成方式。

那么co模块又是干什么的呢?

co模块其实是koa内部实现的核心,写一个展示其基本用法的demo如下:

const co = require('co');                                           
                                                                    
const promisedSetTimeout = i => new Promise((resolve, reject) => {
  setTimeout(() => {                                                
    console.log(i);                                                 
    resolve(i * i);                                                 
  }, 1000);                                                            
});                                                                 
                                                                    
co(function *() {                                                   
  let j = 2;                                                        
  for (let i = 0; i < 5; i++) {                                     
    j = yield promisedSetTimeout(j);                                                              
  }                                                                 
});                                                                 

这段代码每秒钟输出一个数,分别输出​2, 4, 16, 256, 65536

可以看到,co实际上就是将generator自动执行的工具,并且可以通过yield Promise方法的方式实现异步的同步调用。

那么co的实现原理是怎样的呢?

之前一篇文章讲到了express中间件的实现原理,其实co模块的实现原理与之大同小异。

express内部维护了一个中间件方法的列表(迭代器),而generator表示的其实就是列表的本身(生成器),所以generator说白了就是用类似一个function的形式表描述一个列表的工具。

function myco(generator) {                                        
  const g = generator();                                          
  const next = (data) => {                                        
    let middleware = g.next(data);                                
    if (middleware.done) {                                        
      return middleware.value                                     
        .then(ret => Promise.resolve(ret))                        
        .catch(ret => Promise.reject(ret));                       
    }                                                             
    return middleware.value.then(ret => next(ret));               
  }                                                               
  return next();                                                  
}                                                                 
                                                                  
const promisedSetTimeout = i => new Promise((resolve, reject) => {
  setTimeout(() => {                                              
    console.log(i);                                               
    resolve(i * i);                                               
  }, 1000);                                                       
});                                                               
                                                                  
myco(function *() {                                               
  let j = 2;                                                      
  for (let i = 0; i < 5; i++) {                                   
    j = yield promisedSetTimeout(j);                              
  }                                                               
});                                                               

这里有一个小坑,就是generator.next()方法可以传入参数,传入的值会覆盖上一个迭代周期yield返回的值。所以我们yield返回的是一个Promise对象,而next里面传入了Promise.then里面的数据,从而改变了返回值。

co模块还提供了一个co.wrap的方法,用于将generator转换成Promise对象。co.wrap和co的区别在于一个立即执行,一个不立即执行。

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

当然用于生产的co模块远没有上面那么简单,具体可以参考源码: https://github.com/tj/co/tree/4.6.0


3. Koa 1.0的实现

有了上述co和generator的基础,下面就可以hack一下Koa 1.0的源码实现了

Koa基本框架结构和express基本一致,Application类内部维护了一个middleware list、一个连接上下文context以及Request和Response对象,最大的区别实际上是app.callback的实现。

首先Koa1.0下app.use必须传入的是generator实例:


接下来看app.callback长这样:


不难看出定义的fn实际上是一个Promise方法,而这个Promise方法是从中间件列表转换而来的。

co.wrap是将generator转换为Promise对象,而compose是koa核心模块koa-compose的方法,其实现如下:


compose的作用是将所有的generator middlewares合成了一个大的generator,里面用到了yield *的语法,这是用来在generator里面调用generator的方法,具体可以参考 http://es6.ruanyifeng.com/#docs/generator#yield--语句

除此之外,其实compose的方法基本类似上一篇文章中的middleware处理方法。

由此,koa框架就这样实现了~


4. Koa 2.0, 3.0与async/await

在Koa1.0的源码里面经常可以看到这样的语句:

if (this.experimental) {                                                                                                                  
  console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
}                                                                                                                                         

实际上Koa1.0使用experimental模式的话是可以传入返回Promise对象的function作为中间件的,这样做实际上是为了兼容es7中的async/await的用法的。

而随着async/await的推广和标准化,在Koa 2.0里面就规定中间件必须传入返回Promise对象的函数了,而Koa2.0也不再支持使用Generator了。

Koa2.0里面的中间件写法demo如下:

const Koa = require('koa');                              
const co = require('co');                                
const app = new Koa();                                   
                                                         
app.use(co.wrap(function *(ctx, next) {                  
  console.log(1);                                        
  yield next();                                          
  console.log(2);                                        
}));                                                     
                                                         
app.use(async (ctx, next) => {                           
  console.log(3);                                        
  await next();                                          
  console.log(4);                                        
});                                                      
                                                         
app.use((ctx, next) => new Promise((resolve, reject) => {
  console.log(5);                                        
  resolve(next());                                       
  console.log(6);                                        
}));                                                     
                                                         
module.exports = app;                                    

有co.wrap的方法可以直接将Generator转换成() => Promise,实际上很多Koa1.0的中间件外面套一层co.wrap就可以直接放到Koa2.0上用了。

不过在写这篇小心得的时候,Node.js的LTS版本目前在native层还没有支持async/await,所以Koa2.0臃肿的引入了babel来做转换,而Koa3.0就是用来解决这个问题的。


回复

对话列表

×