EventProxy 
English Doc
这个世界上不存在所谓回调函数深度嵌套的问题。 —— Jackson Tian
世界上本没有嵌套回调,写得人多了,也便有了
}}}}}}}}}}}}。 —— fengmk2
- API文档: API Documentation
- jscoverage: 97%
- 源码注解:注解文档
EventProxy 仅仅是一个很轻量的工具,但是能够带来一种事件式编程的思维变化。有几个特点:
- 利用事件机制解耦复杂业务逻辑
- 移除被广为诟病的深度callback嵌套问题
- 将串行等待变成并行等待,提升多异步协作场景下的执行效率
- 友好的Error handling
- 无平台依赖,适合前后端,能用于浏览器和Node.js
- 兼容CMD,AMD以及CommonJS模块环境
现在的,无深度嵌套的,并行的
varep=EventProxy.create("template","data","l10n",function(template,data,l10n){_.template(template,data,l10n);});$.get("template",function(template){// somethingep.emit("template",template);});$.get("data",function(data){// somethingep.emit("data",data);});$.get("l10n",function(l10n){// somethingep.emit("l10n",l10n);});过去的,深度嵌套的,串行的。
varrender=function(template,data){_.template(template,data);};$.get("template",function(template){// something$.get("data",function(data){// something$.get("l10n",function(l10n){// somethingrender(template,data,l10n);});});});通过NPM安装即可使用:
$ npm install eventproxy调用:
varEventProxy=require('eventproxy');$ spm install eventproxy$ component install JacksonTian/eventproxy以下示例均指向Github的源文件地址,您也可以下载源文件到你自己的项目中。整个文件注释全面,带注释和空行,一共约500行。为保证EventProxy的易嵌入,项目暂不提供压缩版。用户可以自行采用Uglify、YUI Compressor或Google Closure Complier进行压缩。
在页面中嵌入脚本即可使用:
<scriptsrc="https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy.js"></script>使用:
// EventProxy此时是一个全局变量varep=newEventProxy();SeaJS下只需配置别名,然后require引用即可使用。
// 配置seajs.config({alias: {eventproxy: 'https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy.js'}});// 使用seajs.use(['eventproxy'],function(EventProxy){// TODO});// 或者define('test',function(require,exports,modules){varEventProxy=require('eventproxy');});RequireJS实现的是AMD规范。
// 配置路径require.config({paths: {eventproxy: "https://raw.github.com/JacksonTian/eventproxy/master/lib/eventproxy"}});// 使用require(["eventproxy"],function(EventProxy){// TODO});此处以页面渲染为场景,渲染页面需要模板、数据。假设都需要异步读取。
varep=newEventProxy();ep.all('tpl','data',function(tpl,data){// or ep.all(['tpl', 'data'], function (tpl, data){})// 在所有指定的事件触发后,将会被调用执行// 参数对应各自的事件名});fs.readFile('template.tpl','utf-8',function(err,content){ep.emit('tpl',content);});db.get('some sql',function(err,result){ep.emit('data',result);});all方法将handler注册到事件组合上。当注册的多个事件都触发后,将会调用handler执行,每个事件传递的数据,将会依照事件名顺序,传入handler作为参数。
EventProxy提供了create静态方法,可以快速完成注册all事件。
varep=EventProxy.create('tpl','data',function(tpl,data){// TODO});以上方法等效于
varep=newEventProxy();ep.all('tpl','data',function(tpl,data){// TODO});此处以读取目录下的所有文件为例,在异步操作中,我们需要在所有异步调用结束后,执行某些操作。
varep=newEventProxy();ep.after('got_file',files.length,function(list){// 在所有文件的异步执行结束后将被执行// 所有文件的内容都存在list数组中});for(vari=0;i<files.length;i++){fs.readFile(files[i],'utf-8',function(err,content){// 触发结果事件ep.emit('got_file',content);});}after方法适合重复的操作,比如读取10个文件,调用5次数据库等。将handler注册到N次相同事件的触发上。达到指定的触发数,handler将会被调用执行,每次触发的数据,将会按触发顺序,存为数组作为参数传入。
此处以股票为例,数据和模板都是异步获取,但是数据会持续刷新,视图会需要重新刷新。
varep=newEventProxy();ep.tail('tpl','data',function(tpl,data){// 在所有指定的事件触发后,将会被调用执行// 参数对应各自的事件名的最新数据});fs.readFile('template.tpl','utf-8',function(err,content){ep.emit('tpl',content);});setInterval(function(){db.get('some sql',function(err,result){ep.emit('data',result);});},2000);tail与all方法比较类似,都是注册到事件组合上。不同在于,指定事件都触发之后,如果事件依旧持续触发,将会在每次触发时调用handler,极像一条尾巴。
通过事件实现异步协作是EventProxy的主要亮点。除此之外,它还是一个基本的事件库。携带如下基本API
on/addListener,绑定事件监听器emit,触发事件once,绑定只执行一次的事件监听器removeListener,移除事件的监听器removeAllListeners,移除单个事件或者所有事件的监听器
为了照顾各个环境的开发者,上面的方法多具有别名。
- YUI3使用者,
subscribe和fire你应该知道分别对应的是on/addListener和emit。 - jQuery使用者,
trigger对应的方法是emit,bind对应的就是on/addListener。 removeListener和removeAllListeners其实都可以通过别名unbind完成。
所以在你的环境下,选用你喜欢的API即可。
更多API的描述请访问API Docs。
在异步方法中,实际上,异常处理需要占用一定比例的精力。在过去一段时间内,我们都是通过额外添加error事件来进行处理的,代码大致如下:
exports.getContent=function(callback){varep=newEventProxy();ep.all('tpl','data',function(tpl,data){// 成功回调callback(null,{template: tpl,data: data});});// 侦听error事件ep.bind('error',function(err){// 卸载掉所有handlerep.unbind();// 异常回调callback(err);});fs.readFile('template.tpl','utf-8',function(err,content){if(err){// 一旦发生异常,一律交给error事件的handler处理returnep.emit('error',err);}ep.emit('tpl',content);});db.get('some sql',function(err,result){if(err){// 一旦发生异常,一律交给error事件的handler处理returnep.emit('error',err);}ep.emit('data',result);});};代码量因为异常的处理,一下子上去了很多。在这里EventProxy经过很多实践后,我们根据我们的最佳实践提供了优化的错误处理方案。
exports.getContent=function(callback){varep=newEventProxy();ep.all('tpl','data',function(tpl,data){// 成功回调callback(null,{template: tpl,data: data});});// 添加error handlerep.fail(callback);fs.readFile('template.tpl','utf-8',ep.done('tpl'));db.get('some sql',ep.done('data'));};上述代码优化之后,业务开发者几乎不用关心异常处理了。代码量降低效果明显。 这里代码的转换,也许有开发者并不放心。其实秘诀在fail方法和done方法中。
ep.fail(callback);// 由于参数位相同,它实际是ep.fail(function(err){callback(err);});// 等价于ep.bind('error',function(err){// 卸载掉所有handlerep.unbind();// 异常回调callback(err);});fail方法侦听了error事件,默认处理卸载掉所有handler,并调用回调函数。
throw 是 ep.emit('error', err) 的简写。
varerr=newError();ep.throw(err);// 实际是ep.emit('error',err);ep.done('tpl');// 等价于function(err,content){if(err){// 一旦发生异常,一律交给error事件的handler处理returnep.emit('error',err);}ep.emit('tpl',content);}在Node的最佳实践中,回调函数第一个参数一定会是一个error对象。检测到异常后,将会触发error事件。剩下的参数,将触发事件,传递给对应handler处理。
done方法除了接受事件名外,还接受回调函数。如果是函数时,它将剔除第一个error对象(此时为null)后剩余的参数,传递给该回调函数作为参数。该回调函数无需考虑异常处理。
ep.done(function(content){// 这里无需考虑异常// 手工emitep.emit('someevent',newcontent);});当然手工emit的方式并不太好,我们更进一步的版本:
ep.done('tpl',function(tpl){// 将内容更改后,返回即可returntpl.trim();});如果emit需要传递多个参数时,ep.done(event, fn)的方式不能满足需求,还是需要ep.done(fn),进行手工emit多个参数。
fail除了用于协助all方法完成外,也能协助after中的异常处理。另外,在after的回调函数中,结果顺序是与用户emit的顺序有关。为了满足返回数据按发起异步调用的顺序排列,EventProxy提供了group方法。
varep=newEventProxy();ep.after('got_file',files.length,function(list){// 在所有文件的异步执行结束后将被执行// 所有文件的内容都存在list数组中,按顺序排列});for(vari=0;i<files.length;i++){fs.readFile(files[i],'utf-8',ep.group('got_file'));}group秉承done函数的设计,它包含异常的传递。同时它还隐含了对返回数据进行编号,在结束时,按顺序返回。
ep.group('got_file');// 约等价于function(err,data){if(err){returnep.emit('error',err);}ep.emit('got_file',data);};当回调函数的数据还需要进行加工时,可以给group带上回调函数,只要在操作后将数据返回即可:
ep.group('got_file',function(data){// some codereturndata;});在node中,emit方法是同步的,EventProxy中的emit,trigger等跟node的风格一致,也是同步的。看下面这段代码,可能眼尖的同学一下就发现了隐藏的bug:
varep=EventProxy.create();db.check('key',function(err,permission){if(err){returnep.emit('error',err);}ep.emit('check',permission);});ep.once('check',function(permission){permission&&db.get('key',function(err,data){if(err){returnep.emit('error');}ep.emit('get',data);});});ep.once('get',function(err,data){if(err){returnep.emit('error',err);}render(data);});ep.on('error',errorHandler);没错,万一db.check的callback被同步执行了,在ep监听check事件之前,它就已经被抛出来了,后续逻辑没办法继续执行。尽管node的约定是所有的callback都是需要异步返回的,但是如果这个方法是由第三方提供的,我们没有办法保证db.check的callback一定会异步执行,所以我们的代码通常就变成了这样:
varep=EventProxy.create();ep.once('check',function(permission){permission&&db.get('key',function(err,data){if(err){returnep.emit('error');}ep.emit('get',data);});});ep.once('get',function(err,data){if(err){returnep.emit('error',err);}render(data);});ep.on('error',errorHandler);db.check('key',function(err,permission){if(err){returnep.emit('error',err);}ep.emit('check',permission);});我们被迫把db.check挪到最后,保证事件先被监听,再执行db.check。check->get->render的逻辑,在代码中看起来变成了get->render->check。如果整个逻辑更加复杂,这种风格将会让代码很难读懂。
这时候,我们需要的就是 异步事件触发:
varep=EventProxy.create();db.check('key',function(err,permission){if(err){returnep.emitLater('error',err);}ep.emitLater('check',permission);});ep.once('check',function(permission){permission&&db.get('key',function(err,data){if(err){returnep.emit('error');}ep.emit('get',data);});});ep.once('get',function(err,data){if(err){returnep.emit('error',err);}render(data);});ep.on('error',errorHandler);上面代码中,我们把db.check的回调函数中的事件通过emitLater触发,这样,就算db.check的回调函数被同步执行了,事件的触发也还是异步的,ep在当前事件循环中监听了所有的事件,之后的事件循环中才会去触发check事件。代码顺序将和逻辑顺序保持一致。 当然,这么复杂的代码,必须可以像ep.done()一样通过doneLater来解决:
varep=EventProxy.create();db.check('key',ep.doneLater('check'));ep.once('check',function(permission){permission&&db.get('key',ep.done('get'));});ep.once('get',function(data){render(data);});ep.fail(errorHandler);最终呈现出来的,是一段简洁且清晰的代码。
- 请勿使用
all作为业务中的事件名。该事件名为保留事件。 - 异常处理部分,请遵循 Node 的最佳实践(回调函数首个参数为异常传递位)。
谢谢 EventProxy 的使用者们,享受 EventProxy 的过程,也给 EventProxy 回馈良多。
project : eventproxy repo age : 3 years, 6 months active : 97 days commits : 203 files : 24 authors : 177 Jackson Tian 87.2% 9 fengmk2 4.4% 7 dead-horse 3.4% 2 azrael 1.0% 2 rogerz 1.0% 1 Bitdeli Chef 0.5% 1 yaoazhen 0.5% 1 Ivan Yan 0.5% 1 cssmagic 0.5% 1 haoxin 0.5% 1 redky 0.5%详情请参见https://github.com/JacksonTian/eventproxy/graphs/contributors
The MIT License。请自由享受开源。
如果您觉得本模块对您有帮助,欢迎请作者一杯咖啡



