所謂自定義事件,就是有別於有別於帶有瀏覽器特定行爲的事件(相似click
, mouseover
, submit
, keydown
等事件),事件名稱能夠隨意定義,能夠經過特定的方法進行添加,觸發以及刪除。數組
先看個簡單的事件添加的例子:瀏覽器
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元素相關的,例如點擊按鈕,文本輸入等,這些爲自帶瀏覽器行爲事件,而自定義事件與這些行爲無關。例如:
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自定義事件的。例如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);