彻底理解thunk函数与co框架
2017-05-12 17:11
309 查看
ES6带来了很多新的特性,其中生成器、yield等能对之前金字塔式的异步回调做到很好地解决,而基于此封装的co框架能让我们完全已同步的方式来编写异步代码。这篇文章就对生成器函数(GeneratorFunction)及框架thunkify、co的核心代码做比较彻底的分析。co的使用还是比较广泛的,除了我们日常的编码要用到外,一些知名框架也是基于co实现的,比如被称为下一代的Nodejs web框架的koa等。
生成器函数
生成器函数是写成:
格式的代码,其本质也是一个函数,所以它具备普通函数所具有的所有特性。除此之外,它还具有以下有用特性:
1. 执行生成器函数后返回一个生成器(Generator),且生成器具有throw()方法,可手动抛出一个异常,也常被用于判断是否是生成器;
2. 在生成器函数内部可以使用yield(或者yield*),函数执行到yield的时候都会暂停执行,并返回yield的右值(函数上下文,如变量的绑定等信息会保留),通过生成器的next()方法会返回一个对象,含当前yield右边表达式的值(value属性),以及generator函数是否已经执行完(done属性)等的信息。每次执行next()方法,都会从上次执行的yield的地方往下,直到遇到下一个yield并返回包含相关执行信息的对象后暂停,然后等待下一个next()的执行;
3. 生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值。
下面用例子说明:
例1:
根据上面说过的第3条执行准则:“生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值”,因为我们没有往生成器的next()中传入任何值,所以:var a = yield ‘a’;中a的值为undefined。
那我们可以将例子稍微修改下:
例2:
这个就比较清晰明了了,不再做过多解释。
关于yield*
yield暂停执行并只返回右值,而yield*则将函数委托到另一个生成器或可迭代的对象(如:字符串、数组、类数组以及ES6的Map、Set等)。举例如下:
arguments
thunk函数
在co的应用中,为了能像写同步代码那样书写异步代码,比较多的使用方式是使用thunk函数(但不是唯一方式,还可以是:Promise)。比如读取文件内容的一步函数fs.readFile()方法,转化为thunk函数的方式如下:
那什么叫thunk函数呢?
thunk函数具备以下两个要素:
1. 有且只有一个参数是callback的函数;
2. callback的第一个参数是error。
使用thunk函数,同时结合co我们就可以像写同步代码那样来写书写异步代码,先来个例子感受下:
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
是不是很酷?真的很酷!
其实,对于每次都去自己书写一个thunk函数还是比较麻烦的,有一个框架thunkify可以帮我们轻松实现,修改后的代码如下:
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
对于thunkify的实现,大概的注释如下:
代码并不复杂,看注释应该就能看懂了。
co框架
我们将整个框架先列出在下面:
对于核心部分,我做注释。下面,我们基于我们之前的例子对co的执行流程做一下分析。
我们的例子是:
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
首先,执行co()函数,内部除了缓存当前执行上下文环境、除generator函数之外的参数处理,主要返回一个Promise实例:
我们主要看这个Promise内部做了什么。
首先,判断co()函数的第一个参数是否是函数,是的话将除gen之外的参数传给该函数并返回给gen;在这里因为gen是一个生成器函数,所以返回一个生成器;
后面判断如果gen此时不是一个生成器,则直接执行Promise的resolve,其实就是将gen传回给:co().then(function(val){});里的val了;
我们这个例子gen是一个生成器,则继续往下执行。
onFulfilled();
后面我们就遇到了co的核心函数:onFulfilled。我们看下这个函数做了什么。
为了防止分心,里面错误的处理我们先暂时不理。
第一次执行该方法,res值为undefined,然后执行生成器的next()方法,对应我们例子里就是执行:
那么ret是一个对象,大概是这样:
然后将ret传给next函数。next函数是:
首先判断生成器内部是否已经执行完,执行完则将执行结果resolve出去。很明显我们例子里才执行到第一个yield,并没有执行完。没执行完,则将ret.value转化为一个Promise实例,我们这里是一个thunk函数,所以toPromise真正执行的是:
执行后其实就是直接返回了一个Promise实例。而这里面,也对fn做了执行,fn是:function(cb){},对应到这里,function(err, res){…}就是被传入到fn中的cb,第一个参数就是error对象,第二个参数res就是读取文件后数据,然后执行resolve,将结果传到下一个then方法的成功函数内,而在这里对应的是:
其实也就是onFulFilled的参数res。根据上面第三条执行准则,我们知道,res是被传入到生成器的next()方法里的,其实也就是对应co内生成器函数参数里的var a = yield readFile(‘a.txt’,{encoding:’utf8’});里的a的值,从而实现了类似于同步的变成范式。
这样,整个基于thunk函数的co框架编程也就理通了,其他的Promise、Generator、GeneratorFunction、Object、Array模式的类似,不再做过多分析。
理解了co的执行逻辑,我们就能更好的掌握其用法,对于后续使用koa等基于co编写的框架我们也能更快速地上手。
co的简版
为了更方便快捷的理解co的执行逻辑,在网络上还有一个简版的实现,如下:
但这个实现,仅支持yield后面是thunk函数的情形。使用示例:
会打印出:
生成器函数
生成器函数是写成:
function* func(){}
格式的代码,其本质也是一个函数,所以它具备普通函数所具有的所有特性。除此之外,它还具有以下有用特性:
1. 执行生成器函数后返回一个生成器(Generator),且生成器具有throw()方法,可手动抛出一个异常,也常被用于判断是否是生成器;
2. 在生成器函数内部可以使用yield(或者yield*),函数执行到yield的时候都会暂停执行,并返回yield的右值(函数上下文,如变量的绑定等信息会保留),通过生成器的next()方法会返回一个对象,含当前yield右边表达式的值(value属性),以及generator函数是否已经执行完(done属性)等的信息。每次执行next()方法,都会从上次执行的yield的地方往下,直到遇到下一个yield并返回包含相关执行信息的对象后暂停,然后等待下一个next()的执行;
3. 生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值。
下面用例子说明:
例1:
function test(){ return 'b'; }
function* func(){ var a = yield 'a'; console.log('gen:',a);// gen: undefined var b = yield test(); console.log('gen:',b);// gen: undefined } var func1 = func(); var a = func1.next(); console.log('next:', a);// next: { value: 'a', done: false } var b = func1.next(); console.log('next:', b);// next: { value: 'b', done: false } var c = func1.next(); console.log('next:', c);// next: { value: undefined, done: true }
根据上面说过的第3条执行准则:“生成器的next()方法返回的是包含yield右边表达式值及是否执行完毕信息的对象;而next()方法的参数是上一个暂停处yield的返回值”,因为我们没有往生成器的next()中传入任何值,所以:var a = yield ‘a’;中a的值为undefined。
那我们可以将例子稍微修改下:
例2:
function test(){ return 'b'; }
function* func(){
var a = yield 'a';
console.log('gen:',a);// gen:1
var b = yield test();
console.log('gen:',b);// gen:2
}
var func2 = func();
var a = func2.next();
console.log('next:', a);// next: { value: 'a', done: false }
var b = func2.next(1);
console.log('next:', b);// next: { value: 'b', done: false }
var c = func2.next(2);
console.log('next:', c);// next: { value: undefined, done: true }
这个就比较清晰明了了,不再做过多解释。
关于yield*
yield暂停执行并只返回右值,而yield*则将函数委托到另一个生成器或可迭代的对象(如:字符串、数组、类数组以及ES6的Map、Set等)。举例如下:
arguments
function* genFunc(){ yield arguments; yield* arguments; } var gen = genFunc(1,2); console.log(gen.next().value); // { '0': 1, '1': 2 } console.log(gen.next().value); // 1 console.log(gen.next().value); // 2 Generator function* gen1(){ yield 2; yield 3; } function* gen2(){ yield 1; yield* gen1(); yield 4; } var g2 = gen2(); console.log(g2.next().value); // 1 console.log(g2.next().value); // 2 console.log(g2.next().value); // 3 console.log(g2.next().value); // 4
thunk函数
在co的应用中,为了能像写同步代码那样书写异步代码,比较多的使用方式是使用thunk函数(但不是唯一方式,还可以是:Promise)。比如读取文件内容的一步函数fs.readFile()方法,转化为thunk函数的方式如下:
function readFile(path, encoding){ return function(cb){ fs.readFile(path, encoding, cb); }; }
那什么叫thunk函数呢?
thunk函数具备以下两个要素:
1. 有且只有一个参数是callback的函数;
2. callback的第一个参数是error。
使用thunk函数,同时结合co我们就可以像写同步代码那样来写书写异步代码,先来个例子感受下:
var co = require('co'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
function readFile(path, encoding){ return function(cb){ fs.readFile(path, encoding, cb); }; }
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'}); console.log(a); // a var b = yield readFile('b.txt', {encoding: 'utf8'}); console.log(b); // b var c = yield readFile('c.txt', {encoding: 'utf8'}); console.log(c); // c return yield Promise.resolve(a+b+c); }).then(function(val){ console.log(val); // abc }).catch(function(error){ console.log(error); });
是不是很酷?真的很酷!
其实,对于每次都去自己书写一个thunk函数还是比较麻烦的,有一个框架thunkify可以帮我们轻松实现,修改后的代码如下:
var co = require('co'), thunkify = require('thunkify'), fs = require('fs'), Promise = require('es6-promise').Promise; var readFile = thunkify(fs.readFile);
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'}); console.log(a); // a var b = yield readFile('b.txt', {encoding: 'utf8'}); console.log(b); // b var c = yield readFile('c.txt', {encoding: 'utf8'}); console.log(c); // c return yield Promise.resolve(a+b+c); }).then(function(val){ console.log(val); // abc }).catch(function(error){ console.log(error); });
对于thunkify的实现,大概的注释如下:
/** * Module dependencies. */ var assert = require('assert'); /** * Expose `thunkify()`. */ module.exports = thunkify; /** * Wrap a regular callback `fn` as a thunk. * * @param {Function} fn * @return {Function} * @api public */ function thunkify(fn) { assert('function' == typeof fn, 'function required'); // 返回一个包含thunk函数的函数,返回的thunk函数用于执行yield,而外围这个函数用于给thunk函数传递参数 return function() { var args = new Array(arguments.length); // 缓存当前上下文环境,给fn提供执行环境 var ctx = this; // 将参数类数组转化为数组(实现方式略显臃肿,可直接用Array.prototype.slice.call(arguments)实现) for (var i = 0; i < args.length; ++i) { args[i] = arguments[i]; } // 真正的thunk函数(有且只有一个参数是callback的函数,且callback的第一个参数为error) // 类似于: // function(cb) {fs.readFile(path, {encoding: 'utf8}, cb)} return function(done) { var called; // 将回调函数再包裹一层,避免重复调用;同时,将包裹了的真正的回调函数push进参数数组 args.push(function() { if (called) return; called = true; done.apply(null, arguments); }); try { // 在ctx上下文执行fn(一般是异步函数,如:fs.readFile) // 并将执行thunkify之后返回的函数的参数(含done回调)传入,类似于执行: // fs.readFile(path, {encoding: 'utf8}, done) // 关于done是做什么用,则是在co库内 fn.apply(ctx, args); } catch (err) { done(err); } } } };
代码并不复杂,看注释应该就能看懂了。
co框架
我们将整个框架先列出在下面:
/** * slice() reference. */ var slice = Array.prototype.slice; /** * Expose `co`. */ module.exports = co['default'] = co.co = co; /** * Wrap the given generator `fn` into a * function that returns a promise. * This is a separate function so that * every `co()` call doesn't create a new, * unnecessary closure. * * @param {GeneratorFunction} fn * @return {Function} * @api public */ co.wrap = function(fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } }; /** * Execute the generator function or a generator * and return a promise. * * @param {Function} fn * @return {Promise} * @api public */ // gen必须是一个生成器函数(会执行该函数并返回生成器)或者是一个生成器(generator函数的返回值) function co(gen) { // 记录上下文环境 var ctx = this; // 除gen之外的其他参数 var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 // 返回一个Promise实例,所以可以以下面这种方式调用co: /** * co(function*(){}).then(function(val){ * * }); * */ return new Promise(function(resolve, reject) { // 如果gen是一个函数则将其置为函数的返回值 if (typeof gen === 'function') { gen = gen.apply(ctx, args); } // 如果gen不是生成器,则直接返回 if (!gen || typeof gen.next !== 'function') { return resolve(gen); } // 核心方法,启动generator的执行 onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ // res记录的是:上一个yield的返回值中value的值({done:false,value:''}中value的值) // ret记录的是:本次yield的返回值(整个{done:false,value:''}) // generator相关:执行生成器的next()方法的时候,会在当前yield处执行完毕并停住, // next()方法返回的是yield执行后的状态(done)及yield 表达式返回的值(value), // 而next()方法内的参数会作为:var a=yield cb();a的值,所以往下看 /** * 假设:co(function*(){ * var a = yield readFile('a.txt'); * console.log(a); * var b = yield readFile('b.txt); * console.log(b); * }); * 那么根据上面generator的理论,res就是a,b的值 * */ function onFulfilled(res) { var ret; try { // 返回的是co里yield后面表达式的值。如果co里yield的是thunk函数那ret.value就是thunk函数 ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ function next(ret) { // 执行完毕的话,如果外层调用的是: /** * co(function*(){ * return yield Promise.resolve(1); * }).then(function(val){ * console.log(val); // 1 * }); * */ // 则ret.value就是上面传递到then成功回调里val的值 if (ret.done) { return resolve(ret.value); } // 还没结束的话将ret.value转化为Promise实例,相当于执行: // promise.then(onFulfilled).then(onFulfilled).then(onFulfilled)... var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) { // 此时onFulfilled里参数传入的就是上一个yield的返回值的value值 return value.then(onFulfilled, 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) + '"')); } }); } /** * Convert a `yield`ed value into a promise. * * @param {Mixed} obj * @return {Promise} * @api private */ function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; } /** * Convert a thunk to a promise. * * @param {Function} * @return {Promise} * @api private */ function thunkToPromise(fn) { var ctx = this; return new Promise(function(resolve, reject) { fn.call(ctx, function(err, res) { if (err) return reject(err); if (arguments.length > 2) res = slice.call(arguments, 1); resolve(res); }); }); } /** * Convert an array of "yieldables" to a promise. * Uses `Promise.all()` internally. * * @param {Array} obj * @return {Promise} * @api private */ function arrayToPromise(obj) { return Promise.all(obj.map(toPromise, this)); } /** * Convert an object of "yieldables" to a promise. * Uses `Promise.all()` internally. * * @param {Object} obj * @return {Promise} * @api private */ function objectToPromise(obj) { var results = new obj.constructor(); 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]); if (promise && isPromise(promise)) defer(promise, key); else results[key] = obj[key]; } return Promise.all(promises).then(function() { return results; }); function defer(promise, key) { // predefine the key in the result results[key] = undefined; promises.push(promise.then(function(res) { results[key] = res; })); } } /** * Check if `obj` is a promise. * * @param {Object} obj * @return {Boolean} * @api private */ function isPromise(obj) { return 'function' == typeof obj.then; } /** * Check if `obj` is a generator. * * @param {Mixed} obj * @return {Boolean} * @api private */ function isGenerator(obj) { return 'function' == typeof obj.next && 'function' == typeof obj.throw; } /** * Check if `obj` is a generator function. * * @param {Mixed} obj * @return {Boolean} * @api private */ function isGeneratorFunction(obj) { var constructor = obj.constructor; if (!constructor) return false; if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; return isGenerator(constructor.prototype); } /** * Check for plain object. * * @param {Mixed} val * @return {Boolean} * @api private */ function isObject(val) { return Object == val.constructor; }
对于核心部分,我做注释。下面,我们基于我们之前的例子对co的执行流程做一下分析。
我们的例子是:
var co = require('co'),
thunkify = require('thunkify'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
function readFile(path, encoding){ return function(cb){ fs.readFile(path, encoding, cb); }; }
//var readFile = thunkify(fs.readFile);
co(function* (){// 外面不可见,但在co内部其实已经转化成了promise.then().then()..链式调用的形式
var a = yield readFile('a.txt', {encoding: 'utf8'}); console.log(a); // a var b = yield readFile('b.txt', {encoding: 'utf8'}); console.log(b); // b var c = yield readFile('c.txt', {encoding: 'utf8'}); console.log(c); // c return yield Promise.resolve(a+b+c); }).then(function(val){ console.log(val); // abc }).catch(function(error){ console.log(error); });
首先,执行co()函数,内部除了缓存当前执行上下文环境、除generator函数之外的参数处理,主要返回一个Promise实例:
// 记录上下文环境 var ctx = this; // 除gen之外的其他参数 var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 // 返回一个Promise实例,所以可以以下面这种方式调用co: /** * co(function*(){}).then(function(val){ * * }); * */ return new Promise(function(resolve, reject) { });
我们主要看这个Promise内部做了什么。
if (typeof gen === 'function') { gen = gen.apply(ctx, args); }
首先,判断co()函数的第一个参数是否是函数,是的话将除gen之外的参数传给该函数并返回给gen;在这里因为gen是一个生成器函数,所以返回一个生成器;
if (!gen || typeof gen.next !== 'function') { return resolve(gen); }
后面判断如果gen此时不是一个生成器,则直接执行Promise的resolve,其实就是将gen传回给:co().then(function(val){});里的val了;
我们这个例子gen是一个生成器,则继续往下执行。
onFulfilled();
后面我们就遇到了co的核心函数:onFulfilled。我们看下这个函数做了什么。
function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); }
为了防止分心,里面错误的处理我们先暂时不理。
第一次执行该方法,res值为undefined,然后执行生成器的next()方法,对应我们例子里就是执行:
var a = yield readFile('a.txt', {encoding: 'utf8'});
那么ret是一个对象,大概是这样:
{ done: false, value: function(cb){ fs.readFile(path, encoding, cb); } }
然后将ret传给next函数。next函数是:
function next(ret) { if (ret.done) { return resolve(ret.value); } var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) { return value.then(onFulfilled, 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) + '"')); }
首先判断生成器内部是否已经执行完,执行完则将执行结果resolve出去。很明显我们例子里才执行到第一个yield,并没有执行完。没执行完,则将ret.value转化为一个Promise实例,我们这里是一个thunk函数,所以toPromise真正执行的是:
function toPromise(obj) { if (!obj) return obj; if (isPromise(obj)) return obj; if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); if ('function' == typeof obj) return thunkToPromise.call(this, obj); if (Array.isArray(obj)) return arrayToPromise.call(this, obj); if (isObject(obj)) return objectToPromise.call(this, obj); return obj; }
/** * Convert a thunk to a promise. * * @param {Function} * @return {Promise} * @api private */ function thunkToPromise(fn) { var ctx = this; return new Promise(function(resolve, reject) { fn.call(ctx, function(err, res) { if (err) return reject(err); if (arguments.length > 2) res = slice.call(arguments, 1); resolve(res); }); }); }
执行后其实就是直接返回了一个Promise实例。而这里面,也对fn做了执行,fn是:function(cb){},对应到这里,function(err, res){…}就是被传入到fn中的cb,第一个参数就是error对象,第二个参数res就是读取文件后数据,然后执行resolve,将结果传到下一个then方法的成功函数内,而在这里对应的是:
if (value && isPromise(value)) { return value.then(onFulfilled, onRejected); }
其实也就是onFulFilled的参数res。根据上面第三条执行准则,我们知道,res是被传入到生成器的next()方法里的,其实也就是对应co内生成器函数参数里的var a = yield readFile(‘a.txt’,{encoding:’utf8’});里的a的值,从而实现了类似于同步的变成范式。
这样,整个基于thunk函数的co框架编程也就理通了,其他的Promise、Generator、GeneratorFunction、Object、Array模式的类似,不再做过多分析。
理解了co的执行逻辑,我们就能更好的掌握其用法,对于后续使用koa等基于co编写的框架我们也能更快速地上手。
co的简版
为了更方便快捷的理解co的执行逻辑,在网络上还有一个简版的实现,如下:
function co(generator) { return function(fn) { var gen = generator(); function next(err, result) { if(err){ return fn(err); } var step = gen.next(result); if (!step.done) { step.value(next); } else { fn(null, step.value); } } next(); } }
但这个实现,仅支持yield后面是thunk函数的情形。使用示例:
var co = require('./co'); // wrap the function to thunk function readFile(filename) {// 辅助传参,yield真正使用的是其返回的thunk函数 return function(callback) { require('fs').readFile(filename, 'utf8', callback); }; }
co(function * () { var file1 = yield readFile('./file/a.txt'); var file2 = yield readFile('./file/b.txt'); console.log(file1); console.log(file2); return 'done'; })(function(err, result) { console.log(result) });
会打印出:
content in a.txt content in b.txt done
相关文章推荐
- 彻底理解thunk函数与co框架
- 函数参数与函数调用(彻底理解值传递与引用传递)
- python 编写web框架中的url处理函数以及个人理解
- 两个函数彻底理解Lua中的闭包
- 函数参数与函数调用(彻底理解值传递与引用传递)转别人
- 彻底理解NodeJs中的回调(Callback)函数
- 函数参数与函数调用(彻底理解值传递与引用传递)
- 彻底理解python中函数内赋值操作和对象的可变性
- 彻底理解函数声明与函数表达式优先级问题
- 理解ATL中的一些汇编代码(通过Thunk技术来调用类成员函数)
- 彻底理解MapReduce shuffle过程原理
- CI框架创建全局函数
- 理解linux的exec系列函数
- 深入理解变量声明提升和函数声明提升
- 让你彻底理解hashCode的作用
- JQuery设置缓慢下拉大行多次执行的解决办法,以及stop()函数的简单理解
- 彻底理解ThreadLocal
- 从头到尾彻底理解傅里叶变换算法、上
- 通俗易懂SpringMVC整体框架理解
- Java 集合框架的再次理解