在写node.js的时候,经常会遇到要去数据库的多个地方取得多个数据,然后才能进行下一步的操作的情况。如果是线性执行的语言,通常的做法是一条一条去取,全部取到之后再进行下一步操作。然而在node里面,因为是基于事件的,所以只能够一层一层的在回调函数里面嵌套进去。取到第一个数据之后,执行回调函数去取第二条数据,然后再执行回调函数。
对于node来说,这样是效率低下的,因为很多数据并不是需要先取完A再去取B的,而是可以同时取AB的,然而通过回调函数嵌套的做法,只能够顺序去取数据(和线性执行的语言一样),同时会导致代码嵌套过多难以阅读。
解决这个问题的一个方法是通过事件的方式。遇到要取多个数据的时候,就让它们都去取数据,取到数据之后,抛出一个取到数据的事件,同时开一个监听函数,监听这些事件,当所有的数据都取到了,监听函数接收到了所有数据的时候,开始执行后面的步骤。这样可以尽可能的让数据获取能够同步进行(如果有限制关系:如先取到文章才能取到文章的评论,同样可以用事件的方式解决,不过依旧要按照顺序去获取数据),同时代码也会简洁易懂,没有多层的嵌套。
然而想法是美好的,node.js提供的eventemit却并不支持监听多个事件。eventProxy模块就是基于事件的对上述问题的一个解决方案。
https://github.com/JacksonTian/eventproxy @朴灵
_callbacks用来存放所有的回调函数,对于一个事件en,每一次绑定都将在_callbacks中的_callbacks[en]这个数组中插入这个回调函数。(_callbacks用object来作为一个集合容器,而_callbacks[ev]用数组方式,在unbind的时候,需要遍历数组去找到要解除绑定的函数)
同样,也可以将事件和回调函数解除绑定。
EventProxy.prototype.unbind = EventProxy.prototype.removeListener = function (ev, callback) { var calls; if ( ! ev) { // 如果没有输入参数,则清除全部回调函数 this ._callbacks = {}; } else if (calls = this ._callbacks) { if ( ! callback) { // 如果输入了event,没输入callback,则删除这个event的所有callbacks calls[ev] = []; } else { var list = calls[ev]; if ( ! list) return this ; for ( var i = 0 , l = list.length; i < l; i ++ ) { if (callback === list[i]) { list[i] = null ; // 最后,将找到的这个回调函数指为null(为什么先指为null而到后面等到trigger的时候再删除?) break ; } } } } return this ; };
这个函数可以看出,js可以很灵活的通过参数传递的不同来实现不同的效果,比之C++的实现方式要容易。
evnetProxy还提供了once函数,注册的回调函数在执行一次之后就会被unbind。
EventProxy.prototype.once = function (ev, callback) { var self = this ; this .bind(ev, function () { callback.apply(self, arguments); self.unbind(ev, arguments.callee); // callee是当前函数的引用 }); return this ; };
同样和eventemit一样,eventProxy也有事件触发函数emit/fire/trigger,触发eventName事件。
EventProxy.prototype.emit = EventProxy.prototype.fire = EventProxy.prototype.trigger = function (eventName, data, data2) { var list, calls, ev, callback, args, i, l; var both = 2 ; // 其实每次都会触发两个事件 if ( ! (calls = this ._callbacks)) return this ; while (both -- ) { ev = both ? eventName : 'all'; // 第一次是触发eventName事件,第二次触发'all'事件,这个事件在后面提及的assign监听多个事件的时候会起到关键作用。 if (list = calls[ev]) { for (i = 0 , l = list.length; i < l; i ++ ) { if ( ! (callback = list[i])) { list.splice(i, 1 ); i -- ; l -- ; // 触发一次这个事件的时候,遍历它,同时将unbind时设置为null的事件从数组删除。 } else { args = both ? Array.prototype.slice.call(arguments, 1 ) : arguments; callback.apply( this , args); // 如果是eventName事件,则将data参数传给callback,如果是'all'事件,则把所有参数传递进去。 } } } } return this ; };
到此为止,已经可以实现node自带的eventEmit的功能了。On/addListener注册事件,加入callback集合中,然后通过emit/fire/trigger来触发这些事件。但是nevetProxy最重要的作用是下面这个_assign方法,它可以同时监听多个事件,只有这些事件都被触发以后才会执行回调函数,同时每一个事件都可以把参数传递给回调函数。
var _assign = function (eventname1, eventname2, cb, once) { var proxy = this , length, index = 0 , argsLength = arguments.length, callback, events, isOnce, times = 0 , flag = {}; // Check the arguments length. if (argsLength < 3 ) { return this ; } events = [].slice.apply(arguments, [ 0 , argsLength - 2 ]); callback = arguments[argsLength - 2 ]; isOnce = arguments[argsLength - 1 ]; // Check the callback type. if ( typeof callback !== " function " ) { return this ; } length = events.length; var bind = function (key) { var method = isOnce ? " once " : " bind " ; // 函数有两种绑定方法,绑定一次或者绑定多次 proxy[method](key, function (data) { proxy._fired[key] = proxy._fired[key] || {}; proxy._fired[key].data = data; if ( ! flag[key]) { flag[key] = true ; Times ++ ; // 计数器,每有一个事件被触发,计数器+1 } }); }; for (index = 0 ; index < length; index ++ ) { // 对所有要监听事件调用上面的bind方法,为其绑定一个回调函数,当触发这个事件的时候,把数据保存下来。 bind(events[index]); } var all = function () { // 当计数器的计数等于事件数目(说明所有事件都被触发了一次),就把所有获取到的数据汇总,然后交给真正的回调函数cb执行。 if (times < length) { return ; } var data = []; for (index = 0 ; index < length; index ++ ) { data.push(proxy._fired[events[index]].data); } if (isOnce) { // 如果只监听一次,则解除这个'all'的监听。反之则继续监听这个事件。 proxy.unbind( " all " , all); } callback.apply( null , data); }; proxy.bind( " all " , all); // 绑定'all'事件和上面的all函数,而每当一个事件被fire的时候,都会抛出一个'all'事件,使得all函数被调用,去检查是否所有被监听的事件都被fire过了。 };
上面的函数被下面两个接口函数调用,实现两种不同的功能。 //当所有事件都发生之后,执行一次cb,然后撤销监听。 EventProxy.prototype.assign = function (eventname1, eventname2, cb) {
assign运用:当在数据库取数据的时候,可以同时去取N个数据,当得到所有的结果之后,再执行后续的处理。 Assignall运用: 1.例如一个股票软件,需要定时更新数据。在模板ok之前来的数据也没有用的。只有在数据和模板都ok之后,才第一次渲染, 这之后,数据每次更新都需要重新渲染。 2.对现有结果进行搜索的场景,之前在过滤数据之后,需要更新dom。 通过eventProxy,可以充分的发挥js的异步/基于事件的特性,同步进行多个操作。同时也可以让代码更加简洁自然,消除多重嵌套。