js自定義事件、DOM/僞DOM自定義事件

所謂自定義事件,就是有別於有別於帶有瀏覽器特定行爲的事件(相似clickmouseoversubmitkeydown等事件),事件名稱能夠隨意定義,能夠經過特定的方法進行添加,觸發以及刪除。數組

JS自定義事件

先看個簡單的事件添加的例子:瀏覽器

element.addEventListener("click", function() {
    alert(1)
});

這是個簡單的爲DOM元素分配事件處理函數的方法(IE 不支持),有別於:數據結構

element.onclick = function() {
   alert(1)
};

addEventListener()能夠爲元素分配多個處理函數(而非覆蓋),所以,咱們能夠繼續:閉包

element.addEventListener("click", function() {
    alert(2)
});

而後,當element被click(點擊)的時候,就會連續觸彈出「1」和「2」。dom

抽象→具象→本質→數據層
你有沒有以爲這種行爲表現有點相似於往長槍裏面塞子彈(add),(扣動扳手 – click)發射的時候按照塞進去的順序依次出來。函數

這種行爲表現爲咱們實現自定義事件提供了思路:咱們能夠定義一個數組,當添加事件的時候,咱們push進去這個事件處理函數;當咱們執行的時候,從頭遍歷這個數組中的每一個事件處理函數,並執行。this

當多個事件以及對應數據處理函數添加後,咱們最終會獲得一個相似下面數據結構的對象:spa

_listener = {
    "click": [func1, func2],
    "custom": [func3],
    "defined": [func4, func5, func6]
}

所以,若是咱們脫離DOM, 純碎在數據層面自定義事件的話,咱們只要以構建、遍歷和刪除_listener對象爲目的便可。prototype

函數式實現
仍是那句話,按部就班,咱們先看看函數式的實現(只展現骨幹代碼):code

var _listener = {};
var addEvent = function(type, fn) {
    // 添加
};
var fireEvent = function(type) {
    // 觸發
};
var removeEvent = function(type, fn) {
    // 刪除
};

上面的代碼雖然顯得比較初級,可是目的亦可實現。例如:

addEvent("alert", function() {
    alert("彈出!");
});

// 觸發自定義alert事件
fireEvent("alert");

可是,函數式寫法缺點顯而易見,過多暴露在外的全局變量(全局變量是魔鬼),方法無級聯等。這也是上面懶得顯示完整代碼的緣由,略知便可。

字面量實現
衆所周知,減小全局變量的方法之一就是使用全局變量(其餘如閉包)。因而,咱們稍做調整

var Event = {
    _listeners: {},    
    // 添加
    addEvent: function(type, fn) {
        if (typeof this._listeners[type] === "undefined") {
            this._listeners[type] = [];
        }
        if (typeof fn === "function") {
            this._listeners[type].push(fn);
        }    
        return this;
    },
    // 觸發
    fireEvent: function(type) {
        var arrayEvent = this._listeners[type];
        if (arrayEvent instanceof Array) {
            for (var i=0, length=arrayEvent.length; i<length; i+=1) {
                if (typeof arrayEvent[i] === "function") {
                    arrayEvent[i]({ type: type });    
                }
            }
        }    
        return this;
    },
    // 刪除
    removeEvent: function(type, fn) {
        var arrayEvent = this._listeners[type];
        if (typeof type === "string" && arrayEvent instanceof Array) {
            if (typeof fn === "function") {
                // 清除當前type類型事件下對應fn方法
                for (var i=0, length=arrayEvent.length; i<length; i+=1){
                    if (arrayEvent[i] === fn){
                        this._listeners[type].splice(i, 1);
                        break;
                    }
                }
            } else {
                // 若是僅僅參數type, 或參數fn邪魔外道,則全部type類型事件清除
                delete this._listeners[type];
            }
        }
        return this;
    }
};

字面量實現雖然減小了全局變量,可是其屬性方法等都是暴露並且都是惟一的,一旦某個關鍵屬性(如_listeners)不當心在某事件處reset了下,則整個全局的自定義事件都會崩潰。

所以,咱們能夠進一步改進,例如,使用原型鏈繼承,讓繼承的屬性(如_listeners)即便出問題也不會影響全局。

原型模式實現

var EventTarget = function() {
    this._listener = {};
};

EventTarget.prototype = {
    constructor: this,
    addEvent: function(type, fn) {
        if (typeof type === "string" && typeof fn === "function") {
            if (typeof this._listener[type] === "undefined") {
                this._listener[type] = [fn];
            } else {
                this._listener[type].push(fn);    
            }
        }
        return this;
    },
    addEvents: function(obj) {
        obj = typeof obj === "object"? obj : {};
        var type;
        for (type in obj) {
            if ( type && typeof obj[type] === "function") {
                this.addEvent(type, obj[type]);    
            }
        }
        return this;
    },
    fireEvent: function(type) {
        if (type && this._listener[type]) {
            var events = {
                type: type,
                target: this    
            };
            
            for (var length = this._listener[type].length, start=0; start<length; start+=1) {
                this._listener[type][start].call(this, events);
            }
        }
        return this;
    },
    fireEvents: function(array) {
        if (array instanceof Array) {
            for (var i=0, length = array.length; i<length; i+=1) {
                this.fireEvent(array[i]);
            }
        }
        return this;
    },
    removeEvent: function(type, key) {
        var listeners = this._listener[type];
        if (listeners instanceof Array) {
            if (typeof key === "function") {
                for (var i=0, length=listeners.length; i<length; i+=1){
                    if (listeners[i] === key){
                        listeners.splice(i, 1);
                        break;
                    }
                }
            } else if (key instanceof Array) {
                for (var lis=0, lenkey = key.length; lis<lenkey; lis+=1) {
                    this.removeEvent(type, key[lenkey]);
                }
            } else {
                delete this._listener[type];
            }
        }
        return this;
    },
    removeEvents: function(params) {
        if (params instanceof Array) {
            for (var i=0, length = params.length; i<length; i+=1) {
                this.removeEvent(params[i]);
            }    
        } else if (typeof params === "object") {
            for (var type in params) {
                this.removeEvent(type, params[type]);    
            }
        }
        return this;    
    }
};

其實上面代碼跟字面量方法相比,就是增長了下面點東西:

var EventTarget = function() {
    this._listener = {};
};

EventTarget.prototype = {
    constructor: this,
    // .. 徹底就是字面量模式實現腳本
};

而後,須要實現自定義事件功能時候,先new構造下:

var myEvents = new EventTarget();
var yourEvents = new EventTarget();

這樣,即便myEvents的事件容器_listener跛掉,也不會污染yourEvents中的自定義事件(_listener安然無恙)。

DOM自定義事件

咱們日常所使用的事件基本都是與DOM元素相關的,例如點擊按鈕,文本輸入等,這些爲自帶瀏覽器行爲事件,而自定義事件與這些行爲無關。例如:

element.addEventListener("alert", function() {
    alert("彈出!");
});

這裏的alert就屬於自定義事件,後面的function就是自定義事件函數。而這個自定義事件是直接綁定在名爲element的DOM元素上的,所以,這個稱之爲自定義DOM事件。

因爲瀏覽器的差別,上面的addEventListener在IE瀏覽器下混不來(attachEvent代替),

所以,爲了便於規模使用,咱們須要新的添加事件方法名(合併addEventListenerattachEvent),例如addEvent, 並附帶事件觸發方法fireEvent, 刪除事件方法removeEvent

如何直接在DOM上擴展新的事件處理方法,以及執行自定義的事件呢?

若是不考慮IE6/7瀏覽器,咱們能夠直接在DOM上進行方法擴展。例如添加個addEvent方法:

HTMLElement.prototype.addEvent = function(type, fn, capture) {
    var el = this;
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};

面代碼中的HTMLElement表示HTML元素。以一個<p>標籤元素舉例,其向上尋找原型對象用過會是這樣:HTMLParagraphElement.prototype → HTMLElement.prototype → Element.prototype → Node.prototype → Object.prototype → null。這下您應該知道HTMLElement所處的位置了吧,上述代碼HTMLElement直接換成Element也是能夠的,可是會讓其餘元素(例如文本元素)也擴展addEvent方法,有些浪費了。

這樣,咱們就可使用擴展的新方法給元素添加事件了,例如一個圖片元素:

elImage.addEvent("click", function() {
    alert("我是點擊圖片以後的彈出!");
});

因爲IE6, IE7瀏覽器的DOM水平較低,沒法直接進行擴展,所以,原型擴展的方法在這兩個瀏覽器下是行不通的。要想讓這兩個瀏覽器也支持addEvent方法,只能是頁面載入時候遍歷全部DOM,而後每一個都直接添加addEvent方法了。

var elAll = document.all, lenAll = elAll.length;
for (var iAll=0; iAll<lenAll; iAll+=1) {
    elAll[iAll].addEvent = function(type, fn) {
        var el = this;
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    };
}

僞DOM自定義事件

這裏的「僞DOM自定義事件」是本身定義的一個名詞,用來區分DOM自定義事件的。例如jQuery庫,其是基於包裝器(一個包含DOM元素的中間層)擴展事件的,既與DOM相關,又不直接是DOM,所以,稱之爲「僞DOM自定義事件」。

原型以及new函數構造不是本文重點,所以,下面這個僅展現:

 1 var $ = function(el) {
 2     return new _$(el);    
 3 };
 4 var _$ = function(el) {
 5     this.el = el;
 6 };
 7 _$.prototype = {
 8     constructor: this,
 9     addEvent: function() {
10         // ...
11     },
12     fireEvent: function() {
13         // ...
14     },
15     removeEvent: function() {
16         // ...
17     }
18 }

因而咱們就可使用相似$(dom).addEvent()的語法爲元素添加事件了(包括不包含瀏覽器行爲的自定義事件)。

自定義事件的添加
若是隻考慮事件添加,咱們的工做其實很簡單,根據支持狀況,addEventListenerattachEvent方法分別添加事件(attachEvent方法後添加事件先觸發)便可:

addEvent: function(type, fn, capture) {
    var el = this.el;
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);        
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, fn);
    }
    return this;
}

顯然,事情不會這麼簡單,有句古話叫作「上山容易下山難」,自定義事件添加容易,可是如何觸發它們呢?——考慮到自定義事件與瀏覽器行爲無關,同時瀏覽器沒有直接的觸發事件的方法。

自定義事件的觸發
又是不可避免的,因爲瀏覽器兼容性問題,咱們要分開說了,針對標準瀏覽器和IE6/7等考古瀏覽器。

1. 對於標準瀏覽器,其提供了可供元素觸發的方法:element.dispatchEvent(). 不過,在使用該方法以前,咱們還須要作其餘兩件事,及建立和初始化。所以,總結說來就是:

document.createEvent()
event.initEvent()
element.dispatchEvent()

舉個板栗:

$(dom).addEvent("alert", function() {
    alert("彈彈彈,彈走魚尾紋~~");
});

// 建立
var evt = document.createEvent("HTMLEvents");
// 初始化
evt.initEvent("alert", false, false);

// 觸發, 即彈出文字
dom.dispatchEvent(evt);

createEvent()方法返回新建立的Event對象,支持一個參數,表示事件類型,具體見下表:

參數 事件接口 初始化方法
HTMLEvents HTMLEvent initEvent()
MouseEvents MouseEvent initMouseEvent()
UIEvents UIEvent initUIEvent()

 自定義事件的刪除
與觸發事件不一樣,事件刪除,各個瀏覽器都提供了對於的時間刪除方法,如removeEventListenerdetachEvent。不過呢,對於IE瀏覽器,還要多刪除一個事件,就是爲了實現觸發功能額外增長的onpropertychange事件:

dom.detachEvent("onpropertychange", evt);
相關文章
相關標籤/搜索