eventproxy,擺脫node.js異步代碼嵌套難以閱讀

 首先要感謝eventproxy提供者:JacksonTian(http://weibo.com/shyvo)。node

用node.js去作一些異步的I/O操做時,很容易會寫成回調函數深度嵌套,例如:git


  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循環將:
一、異步結果保存在proxy._fired中,而後將times+1,標誌flag,這個flag可能本意是爲了告知用戶返回了哪一個異步請求的結果,可是做者並無徹底利用起來。其實能夠做爲 EventProxy.prototype.trigger的一個返回值給用戶。
二、執行unbind方法,將 proxy._callbacks[eventname]下的本回調函數解綁。
第二次執行while循環將:
一、執行上面粗綠色的all函數,根據times和length判斷是否已經所有返回。
*/
基本源代碼的分析結束了,咱們來總結一下把:
理論上來講實例化一次 EventProxy 能夠屢次享用並不會形成衝突,由於其times和length是放在函數內的局部變量,各個方法之間互不干涉。可是也有侷限性,就是eventname標識不能取同名,不然就會衝突了。
EventProxy 的實現很巧妙,雖然代碼互相調來調去讓人頭暈,其實能夠稍微改寫一下代碼,讓EventProxy 只有一個入口調用各個功能模塊,功能模塊之間不要互相調用,充分利用return 返回值,這樣代碼閱讀起來會更加容易。
相關文章
相關標籤/搜索