注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

snoopyxdy的博客

https://github.com/DoubleSpout

 
 
 

日志

 
 

eventproxy,摆脱node.js异步代码嵌套难以阅读  

2011-09-20 15:07:31|  分类: node |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
首先要感谢eventproxy提供者:JacksonTian(http://weibo.com/shyvo)。
用node.js去做一些异步的I/O操作时,很容易会写成回调函数深度嵌套,例如:

 var add= function (v1, v2, v3){
       console.log(v1+v2+v3+'');
    };
var value1,value2,value3
    clinet.get("key1", function (err, data) {
        // do something
value1 = data
        clinet.get("key2", function (err, data) {
            // do something
           value2=data
            clinet.get("key3", function (err, data) {
                //do something
value3 = data
                add(value1, value2, value3);
            });
        });
    });

如果我们使用了eventproxy这个插件,则可以更多关心业务,去掉深度嵌套,并且在一些情况下显著提高效率。
在异步操作返回结果互相不耦合的情况下,使用eventproxy会提高性能。
代码可以改写为:

 var EventProxy = require('./eventproxy');
 var proxy = new EventProxy();
 var add= function (v1, v2, v3){
       console.log(v1+v2+v3+'');
    };
proxy.assign("v1", "v2", "v3", add);
    clinet1.get("key1", function (err, data) {
        //do something
        proxy.trigger("v1", data);
    });
   clinet2.get("data", function (err, data) {
        //do something
        proxy.trigger("v2", data);
    });
    clinet3.get("l10n", function (err, data) {
        //do something
        proxy.trigger("v3", data);
    });

这样是不是很酷?先执行 proxy.assign("v1", "v2", "v3", render);  来注册整个异步回调结束后的事件,然后再同时对每个key去做get操作,这些都是异步的哦,所以会比之前按顺序获取key要快一些,当然这个得配合连接池的应用了,这个例子就使用了clinet1-3这3个redis连接。
整个代码风格易于阅读,而且效率会比之前高出一些,那eventproxy是怎么做到的呢?
我们打开它的源代码来一探究竟把。

assign方法://入口方法
EventProxy.prototype.assign = function (eventname1, eventname2, cb) {  //eventname1-n 是事件名, cb是最后的回调函数
    var args = [].slice.call(arguments);  //因为arguments只是类似数组的特殊数组,所以需要用slice来将arguments转存到args中
    args.push(true); //上面的转存就是为了用push方法,这里将true插入args尾部,这是干什么呢?答案我们下段揭晓。
    _assign.apply(this, args); //调用_assign方法,运行环境是 EventProxy
    return this;
};
_assign函数://顺藤摸瓜,我们来看下_assign函数有什么奥秘
var _assign = function (eventname1, eventname2, cb, once) {  //这里同EventProxy.assign函数一样,只是参数的示例
    var proxy = this, length, index = 0, argsLength = arguments.length,  
        callback, events, isOnce, times = 0, flag = {};    
 //这里定义一些变量,将函数调用环境this赋值为proxy,也就是EventProxy
    if (argsLength < 3) {
        return this;      // 如果传入的参数长度错误,则return
    }
    events = [].slice.apply(arguments, [0, argsLength - 2]);  //将参数eventname1-n转存成events 数组
    callback = arguments[argsLength - 2]; //将回调函数(就是例子中的:add函数)存成callback 
    isOnce = arguments[argsLength - 1];  //答案揭晓拉,之前那个true是用来判断是否一次性的,那什么是一次性呢?我们继续往下看

    if (typeof callback !== "function") {
        return this;
    }
    length = events.length;  //这个length比较重要,是判断需要返回的异步结果总数

    var bind = function (key) {
            var method = isOnce ? "once" : "bind";  //这里会根据是否是一次性来调用不同的方法
/*
proxy[method]函数说明:
因为我们之前传递的是true,所以这里调用 EventProxy.prototype.once 这个方法,将key和回调转存进去
EventProxy.prototype.once 这个方法是干什么的呢?我们下段分析。
回调函数说明:
由于作者没有对EventProxy._fired这个属性做任何注释,根据这里的代码发现EventProxy._fired属性是存放异步返回结果
存放格式key:value,在本例子中则是{v1:value1, v2:value2, v3: value3},
所以在 proxy.trigger("v1", data); 函数中的第一个参数要和proxy.assign("v1", "v2", "v3", add);的前几个名称相同。
最后flag对象设置key为true。同样作者也没有对flag对象做任何说明,我们推断出是表示当前这个key已经成功返回
times++表示异步已经返回了多少个结果,注意这里times的作用环境
*/
            proxy[method](key, function (data) {  
                    proxy._fired[key] = proxy._fired[key] || {};
                    proxy._fired[key].data = data;
                    if (!flag[key]) {
                        flag[key] = true;
                        times++;
                    }
                });
        };

    for (index = 0; index < length; index++) {
        bind(events[index]);  //每一个eventname1-n都调用一次bind函数,并将其值传进去
    }
/*
定义all函数,这个all函数是作为回调函数传入到EventProxy.prototype.bind中的,我们先看下这个all函数是干什么的。
*/
    var all = function () {
        if (times < length) {
            return;   //times代表已返回异步结果,length代表共需要返回多少个异步结果,这里如果未全部返回则终止本函数 
        }
        var data = [];  
        for (index = 0; index < length; index++) {
            data.push(proxy._fired[events[index]].data); //如果异步结果已全返回,按顺序转存EventProxy._fired属性里每个eventname的值
        }
        if (isOnce) {
            proxy.unbind("all", all); //又出现了isOnce,如果是true,则接触绑定,unbind函数之后会介绍
        }
        callback.apply(null, data); //最后执行回调函数,也就是本例中的add(),将data作为参数传给add
    };
    proxy.bind("all", all);   //调用EventProxy.prototype.bind方法
};
EventProxy.prototype.once方法:
/*
这里的ev就是eventname的值,本例中的v1,v2
callback就是上段中的bind函数
紧接着调用EventProxy.prototype.bind方法,将eventname的值和一个回调传递过去,这个回调先执行一次callback函数
再调用EventProxy.prototype.unbind函数传递eventname的值和这个 arguments.callee 即这个回调函数本身。
*/
EventProxy.prototype.once = function (ev, callback) { 
    var self = this;
    this.bind(ev, function () { 
        callback.apply(self, arguments);   //这里的绿色部分,代表上段2个绿色
        self.unbind(ev, arguments.callee);
    });
    return this;
};
EventProxy.prototype.unbind方法:
/**
原版说明:
 * @description Remove one or many callbacks. If `callback` is null, removes all
 * callbacks for the event. If `ev` is null, removes all bound callbacks
 * for all events.
 * @param {string} eventName Event name.
 * @param {function} callback Callback.
总结一下,这个方法作用是根据传递的参数不同来清楚 EventProxy._callback 内的回调函数的
但是使用list[i]=null,这里个人感觉不妥,因为将数组值设置为null以后数组的长度是不会改变的,建议改为:
list.splice(i,1);彻底删除这个数组元素
 */
EventProxy.prototype.unbind = EventProxy.prototype.removeListener = function(ev, callback) {
    var calls;
    if (!ev) {     //如果 eventName不存在,则清空EventProxy._callback对象
        this._callbacks = {};
    } else if (calls = this._callbacks) {  //如果EventProxy._callback存在,这个判断感觉有点多余,可能是为了严谨
        if (!callback) {
            calls[ev] = [];  //如果不传回调函数,则直接清空EventProxy._callback对象中的eventName属性
        } 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;  //判断传递过来的callback在EventProxy._callback对象的eventName属性中是否存在,如果存在则把改callback清空
                break;
            }
        }
        }
    }
    return this;
};
接下来就是核心方法了,
EventProxy.prototype.bind方法
/**
原版说明:
 * @description Bind an event, specified by a string name, `ev`, to a `callback` function.
 * Passing `"all"` will bind the callback to all events fired.
 * @param {string} eventName Event name.
 * @param {function} callback Callback.
 */
EventProxy.prototype.bind = EventProxy.prototype.on = EventProxy.prototype.addListener = function(ev, callback) {
    this._callbacks = this._callbacks || {}; 
    var list  = this._callbacks[ev] || (this._callbacks[ev] = []);  
//给list赋值,如果EventProxy._callbacks[ev]不存在,则将EventProxy._callbacks[ev]设置为数组
    list.push(callback); //将回调函数塞入EventProxy._callbacks[ev]
    return this;
};

又是改变函数作用域,又是方法互相调来调去好不混乱,不管你有没有晕,反正我是晕了。休息一会,让我们简单看一下本例中当执行了proxy.assign("v1", "v2", "v3", add);这个方法后,proxy对象改变了那些呢?proxy是EventProxy的一个实例,所以上面代码中出现的EventProxy都可以用proxy来代替了。
首先是proxy._callbacks,他里面会存放为:
{
   all:(all函数), 
   v1:[(上面紫色的函数)], 
   v2:[(上面紫色的函数)],
   v3:[(上面紫色的函数)]
}

午休片刻,我继续上路。
当异步结果回来以后,EventProxy当地又做了什么呢?我们来看代码:
 clinet1.get("key1", function (err, data) {
        //do something
        proxy.trigger("v1", data);   //异步操作返回以后,调用proxy.trigger传入第一个标识v1和异步结果data;
    });
EventProxy.prototype.trigger方法:
/**
原版说明:
 * @description Trigger an event, firing all bound callbacks. Callbacks are passed the
 * same arguments as `trigger` is, apart from the event name.
 * Listening for `"all"` passes the true event name as the first argument.
 * @param {string} eventName Event name.
 * @param {mix} data Pass in data. 
 */
EventProxy.prototype.emit = EventProxy.prototype.fire = EventProxy.prototype.trigger = function(eventName, data, data2) {
    var list, calls, ev, callback, args, i, l;
    var both = 2;  //好奇怪的2!我们继续看下去!
    if (!(calls = this._callbacks)) return this; //又见容错代码
    while (both--) {   //both是2,这里循环执行2次
        ev = both ? eventName : 'all';  //第一次ev是eventname,第二次是all,上行 while (both--) 相当于while (both) 然后执行 both = both -1
        if (list = calls[ev]) {
            for (i = 0, l = list.length; i < l; i++) {
                if (!(callback = list[i])) {
                    list.splice(i, 1); i--; l--; //这个也属于容错
                } else {
                    args = both ? Array.prototype.slice.call(arguments, 1) : arguments; 
//第一次args是data1-n,第二次是整个参数数组即:eventName,data1-n
                    callback.apply(this, args); 
//第一次依次执行EventProxy._callbacks[eventName]属性数组下的回调,本例只有一个回调执行一次
//第二次依次执行EventProxy._callbacks.all属性数组下的回调,本例只有一个回调执行一次
                }
            }
        }
    }
    return this;
};
/*
看完上面代码是不是有点懂了
没错就像你所想象的那样,我们回头看之前绿色的bind函数和all函数
第一次执行while循环将:
1、异步结果保存在proxy._fired中,然后将times+1,标志flag,这个flag可能本意是为了告知用户返回了哪个异步请求的结果,但是作者并没有完全利用起来。其实可以作为 EventProxy.prototype.trigger的一个返回值给用户。
2、执行unbind方法,将proxy._callbacks[eventname]下的本回调函数解绑。
第二次执行while循环将:
1、执行上面粗绿色的all函数,根据times和length判断是否已经全部返回。
*/
基本源代码的分析结束了,我们来总结一下把:
理论上来说实例化一次 EventProxy 可以多次享用并不会造成冲突,因为其times和length是放在函数内的局部变量,各个方法之间互不干涉。但是也有局限性,就是eventname标识不能取同名,否则就会冲突了。
EventProxy 的实现很巧妙,虽然代码互相调来调去让人头晕,其实可以稍微改写一下代码,让EventProxy 只有一个入口调用各个功能模块,功能模块之间不要互相调用,充分利用return 返回值,这样代码阅读起来会更加容易。

  评论这张
 
阅读(3983)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2016