所謂自定義事件,就是有別於有別於帶有瀏覽器特定行為的事件(類似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
代替),
因此,為了便於規模使用,我們需要新的添加事件方法名(合並addEventListener
和attachEvent
),例如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()
的語法為元素添加事件了(包括不包含瀏覽器行為的自定義事件)。
自定義事件的添加
如果只考慮事件添加,我們的工作其實很簡單,根據支持情況,addEventListener
與attachEvent
方法分別添加事件(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() |
自定義事件的刪除
與觸發事件不同,事件刪除,各個瀏覽器都提供了對於的時間刪除方法,如removeEventListener
和detachEvent
。不過呢,對於IE瀏覽器,還要多刪除一個事件,就是為了實現觸發功能額外增加的onpropertychange
事件:
dom.detachEvent("onpropertychange", evt);