标签:
我在学习generator ,yield ,co,thunkify的时候,有许多费解的地方,经过了许多的实践,也慢慢学会用,慢慢的理解,前一阵子有个其他项目的同事过来我们项目组学习node,发现他问的问题和我学习node的时候,遇到的困难都一样,所以产生写篇blog记录下co,thunkify的运用和原理,和园子里的神仙们交流交流,不对之处,还请指正,谢谢。
我在node的编写中,认真敲着敲着代码,然后回过头来发现,代码变成像这样子了,
var fs = require(‘fs‘);
//TODO:读取3个文件,然后... fs.readFile(‘file1.txt‘,‘utf-8‘, function(err,data1){ fs.readFile(‘file1.txt‘,‘utf-8‘, function(err,data2){ fs.readFile(‘file1.txt‘,‘utf-8‘, function(err,data3){ ... }) }) })
或者在前端的代码中也有,
$.get("/b.txt", function(r1){ $.get("/b.txt", function(r2){ $.get("/b.txt", function(r3){ $("div").html(r1+r2+r3); }); }); });
这样子的代码的维护和代码量成倍数上涨,江湖人称回调金字塔,或邪恶金字塔,或者你可能说fs模块和jq,都可以提供同步的方式去调用接口,fs.readFileSync, 设置 async:true,通过同步的执行代码,但是javascript作为一种异步编程的语言,若使用同步的等待进行执行代码,那麽也将失去javascript原有的魅力。所以智慧无穷的人们通过一些新的技术或者新的运用去发挥js的异步编程的特性。
等等。
这些方法都可以去避免 回调金字塔的问题,在开发的成本上参差不齐。
而我再对generator函数做流程控制的时候,用的co,thunkify,用return callback(err,res);进行然后上一级调用的函数表示费解,而且不止co中有return callback(err,res); 在async 包也有,所以想弄清楚co中,究竟是怎么样去做流程控制的。
co,thunkify 的普通调用方式:
Demo1:
1 co(function*(){ 2 //打印的顺序为 timeout!alen ,1,yield 会开启一个协程并开始等待返回 3 yield thunkify(genFun)("timeout! alen"); 4 console.log("over!") 5 }).catch(function(e){ 6 console.log(e.stack); 7 }) 8 9 function genFun (param,callback){ 10 co(function*(){ 11 setTimeout(function () { 12 console.log(param); 13 return callback(null,1); 14 }, 4000); 15 }).catch(function(e){ 16 console.log(e.stack); 17 }) 18 } 19 // 运行结果: 20 //timeout! alen 21 // 1
这里开始运行时,注意:
In computer programming, a thunk is a subroutine that is created, often automatically, to assist a call to another subroutine. Thunks are primarily used to represent an additional calculation that a subroutine needs to execute, or to call a routine that does not support the usual calling mechanism. They have a variety of other applications to compiler code generation and in modular programming.
在计算机编程中,thunk 就是一个自动被创建去帮助调用另一个子进程的子进程。Thunk 主要是被用于表示一个子进程需要执行的额外计算,或者调用一个不支持普通调用机制的子进程。Thunk 在代码编译生成和模块化编程中有这广泛的运用。
这段话说的有点晕,thunk最初产生是被用解决函数的 传值调用和传名调用,而用于js,其实就是把多个参数thunk化成只接受一个参数的函数.
Demo2:
1 //demo1: 2 var thunkify = function(fn) { 3 4 //thunkify 就是直接返回一个函数,在这个函数里面收集arguments这个类数组对象的参数,保存起来 5 return function() { 6 var args = new Array(arguments.length); 7 var ctx = this; 8 9 for (var i = 0; i < args.length; ++i) { 10 args[i] = arguments[i]; 11 } 12 13 /** 14 * 这里返回的函数是给co用的,把上一级收集的参数数组args的末尾加上一个function函数返回的回调函数, 15 * 这个也是为什么 return callback(null,res); 可以返回到上一级调用的函数,因为每个yield 后的结果都会转成promise 16 * ,在function thunkToPromise(fn)函数封装done 17 */ 18 //TODO: 返回一个只有一个done的函数,其他的参数通过args变量用apply调用传入。 19 return function(done) { 20 var called; 21 22 args.push(function() { 23 if (called) return; 24 called = true; 25 done.apply(null, arguments); 26 }); 27 28 try { 29 fn.apply(ctx, args); 30 } catch (err) { 31 done(err); 32 } 33 } 34 }; 35 }
所有将多个函数封装成只接受一个参数函数(详情请看 Thunk的含义),所以使用thunkify 封装函数的时候,
yield thunkify(genFun)("timeout! alen")
返回的是一个function(done){}, 可以看到
demo3:
1 var co = require("co") 2 var thunkify = require("thunkify") 3 4 // 一个构造 5 function Person(age){ 6 this.age = age; 7 } 8 // 一个获取通过名字获取姓名的方法 9 Person.prototype.getAgeName = function(name,callback){ 10 var _this = this; 11 co(function*(){ 12 setTimeout(function(){ 13 return callback(null,name+":"+_this.age); 14 },2000); 15 }).catch(function(e){ 16 console.log(e.stack); 17 throw new Error(e.stack); 18 }) 19 } 20 21 // 开始调用 22 co(function*(){ 23 var p = new Person(13); 24 // 分别是两种不同的调用方法 25 // var res = yield thunkify(p.getAgeName)("xiaoming"); # 1 26 var res = yield thunkify(p.getAgeName).call(p,"xiaoming"); # 2 27 console.log(res) 28 }).catch(function(e){ 29 console.log(e.stack) 30 }) 31 32 // 结果: 33 // #1:undefined 34 // #2:xiaoming:13
正是需要用call去改变thunkify返回的函数里的上下文,所以才需要在里面保存this的调用环境。
return callback(err,res); 可以做到返回,而且最初接触thunkify,co的时候,发现,callback 这个变量根本就是我们直接声明的! 注意是声明,不是定义,因为我们仅仅是为了不让js解析器在语法检查的时候不抛出callback undefined 的错误,然后我们就自己在函数的参数,在实际的调用的是我们根本就没有传=.=,废话少说上图,
是的,的确只是为了避免callback undefined 抛错而什么声明的,但是,它却是有赋值,在thunkfify中,下面:
args.push(function{}) 这个push的元素就是我们的callback,在这里真正赋值。但是这个done参数,又是怎么工作的呢?
1 /** 2 * Execute the generator function or a generator 3 * and return a promise. 4 * 5 * @param {Function} fn 6 * @return {Promise} 7 * @api public 8 */ 9 10 /** 11 * 执行 generator 函数 或者 一个 generator 同时返回一个promise 12 */ 13 14 function co(gen) { 15 var ctx = this; 16 var args = slice.call(arguments, 1); 17 18 // we wrap everything in a promise to avoid promise chaining, 19 // which leads to memory leak errors. 20 // see https://github.com/tj/co/issues/180 21 return new Promise(function(resolve, reject) { 22 if (typeof gen === ‘function‘) gen = gen.apply(ctx, args); 23 if (!gen || typeof gen.next !== ‘function‘) return resolve(gen); 24 25 onFulfilled(); 26 27 /** 28 * @param {Mixed} res 29 * @return {Promise} 30 * @api private 31 */ 32 function onFulfilled(res) { 33 var ret; 34 try { 35 ret = gen.next(res); 36 } catch (e) { 37 return reject(e); 38 } 39 next(ret); 40 } 41 42 /** 43 * @param {Error} err 44 * @return {Promise} 45 * @api private 46 */ 47 48 function onRejected(err) { 49 var ret; 50 try { 51 ret = gen.throw(err); 52 } catch (e) { 53 return reject(e); 54 } 55 next(ret); 56 } 57 58 /** 59 * Get the next value in the generator, 60 * return a promise. 61 * 62 * @param {Object} ret 63 * @return {Promise} 64 * @api private 65 */ 66 /* 把在generator 中next()的值传进去,然后返回一个promise*/ 67 function next(ret) { 68 if (ret.done) return resolve(ret.value); 69 var value = toPromise.call(ctx, ret.value); 70 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 71 return onRejected(new TypeError(‘You may only yield a function, promise, generator, array, or object, ‘ 72 + ‘but the following object was passed: "‘ + String(ret.value) + ‘"‘)); 73 } 74 75 }); 76 }
co,做了什么事情?
1 co(function*(){ 2 // TODO:1 3 }); 4 co(function*(){ 5 // TODO:2 6 })
上述代码 1,2两处,不能保证一定是1先于比2完成。(估计也没人这么写=。=)
gen = gen.apply(ctx, args); 开始调用我们传入的co中的generator 函数,
所以第一次调用onFulfilled(),ret = gen.next(res);返回ret.value 是一个function ,而这个function 正是接受一个done为参数的函数。
然后去到next(ret),
1 /* 把在generator 中next()的值传进去,然后返回一个promise*/ 2 function next(ret) { 3 if (ret.done) return resolve(ret.value); 4 var value = toPromise.call(ctx, ret.value); 5 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 6 return onRejected(new TypeError(‘You may only yield a function, promise, generator, array, or object, ‘ 7 + ‘but the following object was passed: "‘ + String(ret.value) + ‘"‘)); 8 }
在这里,会依次判断是否已经完成,done==true, 否则,res.done =false,ret.value = function(done){},
那麽将ret.value 转成一个promise,而this指向ctx,调用co的上下文,而在value也是一个Promise 之后,然后then(onFulfilled, onRejected),唯有这次value响应了,value的状态不在是pendding了,那麽才执行gen.next(res)的回调,所以
1 /* 把一个被yield 过的值 转成promise */ 2 function toPromise(obj) { 3 if (!obj) return obj; 4 if (isPromise(obj)) return obj; 5 if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); 6 if (‘function‘ == typeof obj) return thunkToPromise.call(this, obj); 7 if (Array.isArray(obj)) return arrayToPromise.call(this, obj); 8 if (isObject(obj)) return objectToPromise.call(this, obj); 9 return obj; 10 }
在这里,obj是一个function,所以会 return thunkToPromise.call(this, obj); 转成promise,并且吧obj 传进去,这个obj正是我们有一个done为参数的函数,
1 /* 把thunk函数转成promise */ 2 function thunkToPromise(fn) { 3 var ctx = this; 4 return new Promise(function (resolve, reject) { 5 fn.call(ctx, function (err, res) { 6 if (err) return reject(err); 7 if (arguments.length > 2) res = slice.call(arguments, 1); 8 resolve(res); 9 }); 10 }); 11 }
到这里,终于看到了done被赋值什么了,是个function(resolve, reject),这里直接返回一个Promise,这个Promise 做了什么?fn被调用,上下文是co调用的环境,done参数是一个回调函数,第一个参数是err,那麽这个和我们上面只是声明却没有定义的有什么关系?
我们申明的callback,在我们thunkify的是时候,就已经赋值了,
A,B,C 三张图,总结一下
结果:
所以,看到如果函数有return 那么,这个generator 函数会直接返回{done:true,value=XX}这也是为什么return callback(err,res);我们以下就直接返回上一级函数了。yield thunkify() 唯有遇到return,generator中的所有yield都完成了, 或在本执行函数的过程中遇到reject,才会返回。
node中的流程控制中,co,thunkify为什么return callback()可以做到流程控制?
标签:
原文地址:http://www.cnblogs.com/Alencong/p/5869378.html