thunkify 和 thunkify-wrap 的源码学习

最近一直都在看些源码,源码学习不仅能提高自身的代码阅读能力,还能学习优秀框架的设置思路,所以多多益善啊!今天要介绍的是 thunkifythunkify-wrap 框架,前者是大神TJ开发的,后者是国内Node社区的活跃贡献者dead-horse开发的。本文主要是作为笔者学习的一个记录,如果有表达不准确或者错误的地方,还望指出。

thunkify介绍

在介绍前我们先来看个栗子:

1
2
3
4
5
6
7
function read(file, cb) {
fs.readFile(file, cb);
}
function cb(err, content) {
console.log(content);
}
read('package.json', cb);

read是一个异步读取文件的函数,cb为读取完后的回调函数,很普通也很常见。那么,我们来把read改造下:

1
2
3
4
5
6
7
8
9
function read(file) {
return function(cb) {
fs.readFile(file, cb);
}
}
function cb(err, content) {
console.log(content);
}
read('package.json')(cb);

改造后的read返回一个函数,且只有一个参数,回调函数成了返回函数的参数。
thunkify的功能就是把一个普通的函数改造成一个thunk[θʌŋk]函数。在这里,read就是一个thunk函数。那么,thunkify到底有什么用处呢?

thunkify实战

thunkify的真正价值,是结合 co 框架才能体现出来(其实co本身也是一个thunk函数),现在正流行的 koa 框架就是基于co框架开发的。我们看一段结合co框架的代码:

1
2
3
4
5
6
7
8
9
10
var thunkify = require('thunkify');
var fs = require('fs');
var co = require('co');
var readFile = thunkify(fs.readFile);
co(function* () {
var data = yield readFile('package.json');
})(function(err, content) {
console.log(content);
});

yield后面返回了一个改造后的readFile函数,在这里还未真正执行这个异步函数,那么到底是在哪里调用的呢?先别急,我们来看看co内部到底是怎么实现的,下面是一个简易的co框架(实现原理详解):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function co(GenFunc) {
return function(cb) {
var gen = GenFunc();
next();
function next(err, args) {
if (err) {
cb(err);
} else {
if (gen.next) {
var ret = gen.next(args);
if (ret.done) {
cb && cb(null, args);
} else {
ret.value(next);
}
}
}
}
}
}

请注意第14行代码ret.value(next),这是co框架的核心所在。上文中readFile函数也在这里得到了调用。因为这里ret.value的值就是readFile这个thunk函数,co中的next函数也通过参数传递得以继续执行。再次调用next方法时,args已经是package.josn的内容了。我们继续往下执行,通过gen.next(args),把读取到的内容赋值给外面的data变量。此时ret.done的值为true,因为没有yield了,那么执行回调函数cb。我们前面已经说过,co本身也是一个thunk函数,所以这里的cb就是function(err, content) {console.log(content);}package.josn的内容最终也在这里得到了输出。是不是觉得很奇妙,这就是thunk函数的魅力所在。
在阅读thunkify源码的时候,其中有个called变量产生了些疑惑,不过后来也在这里找到了解释。为了防止多次调用ret.value而造成错误,做了不得已的取舍。

thunkify-wrap介绍

thunkify-wrap框架是对thunkify的一个扩展。在原有基础上:

  1. 增加了多个函数封装成thunk函数
  2. 把函数封装成一个GeneratorFunction函数
  3. 增加事件支持
  4. 允许传递上下文(ctx)

对多个函数的封装:

1
2
3
4
5
6
7
8
9
var thunkify = require('thunkify-wrap');
var user = {
add: function() {},
show: function() {},
list: function() {}
};
module.exports = thunkify(user);
// module.exports = thunkify(user, ['add', 'show']);
// module.exports = thunkify(user, 'add');

传递user对象,内部通过使用for循环,使对象中的每一个函数经过thunkify函数包装成为一个thunk函数。后两句指对其中addshow进行处理和只对add进行处理。
在介绍如何封装成一个GeneratorFunction函数之前,我们先来了解下generator中一个重要的概念,代理(delegating yield)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* gen() {
yield 1;
yield 2;
var data = yield* gen2();
console.log(data);
yield 6;
}
function* gen2() {
console.log('start');
yield 3;
yield 4;
return 'over';
yield 5;
}
var g = gen();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // start { value: 3, done: false }
console.log(g.next()); // { value: 4, done: false }
console.log(g.next()); // over { value: 6, done: false }
console.log(g.next()); // {value: undefined, done: true}

在执行到第三个next的时候,直接输出了start并且在yield 3处暂停了。等到gen2中的return返回,再继续执行gen1中余下的yield
我们通过一个栗子来看下thunkify-wrap是怎么结合co框架来执行一个GeneratorFunction的:

1
2
3
4
5
6
7
8
9
10
var genify = require('thunkify-wrap').genify;
var fs = require('fs');
var co = require('co');
var readFile = genify(fs.readFile);
co(function* () {
var data = yield* readFile('package.json', 'utf8');
})(function(err, content) {
console.log(content);
});

注意第5行代码,fs.readFile经过genify的处理后,此时已经是一个GeneratorFunction,因为在thunkify-wrap内部,判断如果传入的fn已经是GeneratorFunction了,直接返回,如果不是,则返回一个GeneratorFunction。详见代码:

1
2
3
4
5
6
7
8
9
10
11
function genify(fn, ctx) {
if (isGeneratorFunction(fn)) {
return fn;
}
function* genify() {
var thunk = thunkify(fn);
var data = yield thunk.apply(ctx || this, arguments);
return data;
}
return genify;
}

所以上面的代码可以改为类似这样:

1
2
3
4
5
6
7
8
9
10
var readFile = (function* () {
var thunk = thunkify(fn);
var data = yield thunk.apply(ctx || this, arguments);
return data;
});
co(function* () {
var data = yield* readFile('package.json', 'utf8');
})(function(err, content) {
console.log(content);
});

是不是觉得和上面的delegating yield的栗子差不多了。在co中的var ret = gen.next(args)第一次调用时,程序通过代理在readFile中的yield处暂停,此时ret.value的值为fs.readFile函数。注意此时的fs.readFile已经通过thunkify处理成一个thunk函数了,执行ret.value(next)语句进行fs.readFile的调用,co中的next方法通过回调函数的形式再次被调用,而此时的args已经是package.json的内容了。第二次执行gen.next(args)时把文件内容赋值给外部readFile中的变量data,通过return返回到第一个GeneratorFunction中。因为没有yield了,所以co中ret.done的值为true,直接调用回调函数输出文件内容。

小结

thunkify和thunkify-wrap框架是co框架的基石,也是koa框架的重要组成部分,所以先了解他们的实现对后面学习koa还是很有帮助的。起初学习的时候,代码看的会有些晕,但了解了他在co中的实现后,就能更好的理解他为什么要设计成这样。恩,that’s all.