1. 程式人生 > >js自定義事件、DOM/偽DOM自定義事件

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

多個 cap style 漸進 ash spa window false .proto

所謂自定義事件,就是有別於有別於帶有瀏覽器特定行為的事件(類似click, mouseover, submit, keydown等事件),事件名稱可以隨意定義,可以通過特定的方法進行添加,觸發以及刪除。

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”。

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

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

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

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

因此,如果我們脫離DOM, 純碎在數據層面自定義事件的話,我們只要以構建、遍歷和刪除_listener對象為目的即可。

函數式實現
還是那句話,循序漸進,我們先看看函數式的實現(只展示骨幹代碼):

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.prototypeHTMLElement.prototypeElement.prototypeNode.prototypeObject.prototypenull。這下您應該知道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);

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