一、說明,、引言
我JS還是比較薄弱的,,本文的內(nèi)容屬于邊學(xué)邊想邊折騰的碎碎念,可能沒什么條理,,可能有表述不準確的地方,,可能內(nèi)容比較拗口生僻。如果您時間緊迫,,或者JS造詣已深,,至此您就可以點擊右側(cè)廣告(木有?則RSS或盜版)然后撤了,。
事件是個大課題,,真要從斷奶開始講起的話,可以寫個12期的連載,。關(guān)于JS事件的文章(類似DOM/BOM事件模型,,IE與其他瀏覽器事件差異,DOM1/DOM2事件定義等)落葉般隨處可見,。熟豆子反復(fù)炒一點意思都沒有,,因此,這里談?wù)勛约焊信d趣的自定義事件以及周邊,。
所謂自定義事件,,就是有別于有別于帶有瀏覽器特定行為的事件(類似click
, mouseover
, submit
, keydown
等事件),事件名稱可以隨意定義,,可以通過特定的方法進行添加,,觸發(fā)以及刪除,。
二、JS自定義事件
循序漸進便于接收,。慢慢來~~
先看個簡單的事件添加的例子:
element.addEventListener("click", function() {
// 我是臨時工
});
這是個簡單的為DOM元素分配事件處理函數(shù)的方法(IE 不支持),,有別于:
element.onclick = function() {
// 我是臨時工
};
addEventListener()
可以為元素分配多個處理函數(shù)(而非覆蓋),因此,,我們可以繼續(xù):
element.addEventListener("click", function() {
// 我是二代臨時工
});
然后,,當element
被click(點擊)的時候,就會連續(xù)觸發(fā)“臨時工”和“二代臨時工”函數(shù),。
抽象→具象→本質(zhì)→數(shù)據(jù)層
你有沒有覺得這種行為表現(xiàn)有點類似于往長槍里面塞子彈(add),,(扣動扳手 – click)發(fā)射的時候按照塞進去的順序依次出來。這種行為表現(xiàn)為我們實現(xiàn)自定義事件提供了思路:我們可以定義一個數(shù)組,,當添加事件的時候,,我們push進去這個事件處理函數(shù);當我們執(zhí)行的時候,,從頭遍歷這個數(shù)組中的每個事件處理函數(shù),,并執(zhí)行,。
當多個事件以及對應(yīng)數(shù)據(jù)處理函數(shù)添加后,,我們最終會得到一個類似下面數(shù)據(jù)結(jié)構(gòu)的對象:
_listener = { "click": [func1, func2], "custom": [func3], "defined": [func4, func5, func6] }
因此,如果我們脫離DOM, 純碎在數(shù)據(jù)層面自定義事件的話,,我們只要以構(gòu)建,、遍歷和刪除_listener
對象為目的即可。
函數(shù)式實現(xiàn)
還是那句話,,循序漸進,,我們先看看函數(shù)式的實現(xiàn)(只展示骨干代碼):
var _listener = {}; var addEvent = function(type, fn) { // 添加 }; var fireEvent = function(type) { // 觸發(fā) }; var removeEvent = function(type, fn) { // 刪除 };
上面的代碼雖然顯得比較初級,但是目的亦可實現(xiàn),。例如:
addEvent("alert", function() {
alert("彈出,!");
});
// 觸發(fā)自定義alert事件
fireEvent("alert");
但是,函數(shù)式寫法缺點顯而易見,,過多暴露在外的全局變量(全局變量是魔鬼),,方法無級聯(lián)等。這也是上面懶得顯示完整代碼的原因,,略知即可,。
字面量實現(xiàn)
眾所周知,減少全局變量的方法之一就是使用全局變量(其他如閉包),。于是,,我們稍作調(diào)整(代碼較長,為限制篇幅,,使用了滾動條,,完整顯示點擊這里 – JS交互, RSS中無效果):
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; }, // 觸發(fā) 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類型事件下對應(yīng)fn方法 for (var i=0, length=arrayEvent.length; i<length; i+=1){ if (arrayEvent[i] === fn){ this._listeners[type].splice(i, 1); break; } } } else { // 如果僅僅參數(shù)type, 或參數(shù)fn邪魔外道,,則所有type類型事件清除 delete this._listeners[type]; } } return this; } };
使用類似下面:
Event.addEvent("alert", function() { alert("彈出!"); }); // 觸發(fā)自定義alert事件 Event.fireEvent("alert");
您可以狠狠地點擊這里:JS自定義事件字面量書寫demo
默認頁面document
通過Event.addEvent()
綁定了兩個自定義的alert
事件,,因此,,此時您點擊頁面的空白區(qū)域(非按鈕與示例代碼區(qū)域),就會有如下圖所示的連續(xù)兩個alert框:
demo頁面還有兩個按鈕,,用來清除已經(jīng)綁定的alert
事件,。第一個按鈕清除所有alert
事件,而點擊第二個按鈕清除第一個alert
事件,。例如我們點擊第二個按鈕:
清除完畢后再點擊頁面的空白區(qū)域,, 您會發(fā)現(xiàn)只會彈出“第二個彈出!”字樣的彈出框了,。這表明,,第一個綁定自定義事件被remove掉了。
字面量實現(xiàn)雖然減少了全局變量,,但是其屬性方法等都是暴露而且都是唯一的,,一旦某個關(guān)鍵屬性(如_listeners
)不小心在某事件處reset了下,則整個全局的自定義事件都會崩潰,。因此,,我們可以進一步改進,例如,,使用原型鏈繼承,,讓繼承的屬性(如_listeners
)即使出問題也不會影響全局。
原型模式實現(xiàn)
代碼如下(相比上面增加了addEvents
, fireEvents
, removeEvents
多事件綁定,、執(zhí)行與刪除方法,,篇幅較長,增加滾動限高,,點擊這里完整展示 – JS交互, RSS中無效果)(一堆代碼看得頭大,,建議直接跳過):
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] === listener){ 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,
// .. 完全就是字面量模式實現(xiàn)腳本
};
然后,,需要實現(xiàn)自定義事件功能時候,先new
構(gòu)造下:
var myEvents = new EventTarget(); var yourEvents = new EventTarget();
這樣,,即使myEvents
的事件容器_listener
跛掉,,也不會污染yourEvents
中的自定義事件(_listener
安然無恙)。
您可以狠狠地點擊這里:原型模式下的JS自定義事件demo
從demo右半?yún)^(qū)域的源代碼展示可以看出如何使用addEvents
, fireEvents
方法同時添加和觸發(fā)多個自定義事件的,。
//zxx: 下面為廣告~~注意不要勿點~~嘻嘻~~
三,、DOM自定義事件
我們平常所使用的事件基本都是與DOM元素相關(guān)的,例如點擊按鈕,文本輸入等,,這些為自帶瀏覽器行為事件,,而自定義事件與這些行為無關(guān)。例如:
element.addEventListener("alert", function() { alert("彈出,!"); });
這里的alert
就屬于自定義事件,,后面的function
就是自定義事件函數(shù)。而這個自定義事件是直接綁定在名為element
的DOM元素上的,,因此,,這個稱之為自定義DOM事件。
由于瀏覽器的差異,,上面的addEventListener
在IE瀏覽器下混不來(attachEvent
代替),,因此,為了便于規(guī)模使用,,我們需要新的添加事件方法名(合并addEventListener
和attachEvent
),,例如addEvent
, 并附帶事件觸發(fā)方法fireEvent
, 刪除事件方法removeEvent
,(命名均參考自MooTools庫),。
如何直接在DOM上擴展新的事件處理方法,,以及執(zhí)行自定義的事件呢?
如果不考慮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); }); } };
//zxx: 上面代碼中的HTMLElement
表示HTML元素。以一個<p>
標簽元素舉例,,其向上尋找原型對象用過會是這樣:HTMLParagraphElement.prototype
→HTMLElement.prototype
→ Element.prototype
→ Node.prototype
→ Object.prototype
→ null
,。這下您應(yīng)該知道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擴展自定義方法demo
點擊demo頁面張含韻小姐年輕時候相片,就會有該圖片alt
屬性值,。
測試代碼如下(demo頁面有代碼完整展示):
<img id="image" src="http://image./image/study/s/s256/mm1.jpg" alt="年輕的張含韻" />
document.getElementById("image").addEvent("click", function() {
alert("這是:" + this.alt);
});
只能點到為止
直接在DOM上進行事件方法擴展其實是個糟糕的做法,,因此,,這里我并沒有對自定義事件做進一步深入探討(這個下一部分會講)。
基于DOM擴展缺點有:缺少標準無規(guī)律,、提高沖突可能性,、性能以及瀏覽器支持。
擴展名字任意命,,很有可能就會與未來DOM瀏覽器本身支持的方法相互沖突,;擴展無規(guī)律,很有可能出現(xiàn)A和B同名不同功能的擴展而造成沖突,;IE6-7瀏覽器下所有擴展都要通過遍歷支持,,其性能開銷可想而知;另外IE8對DOM擴展的支持并不完整,,例如其支持Element.prototype
,,卻沒有HTMLElement.prototype
.
雖然我從事的站點就是基于MooTools庫的,但是,,我對MooTools庫基于DOM擴展方法的做法是不支持的,。相反,我更親近jQuery庫的做法,,也就是下面要講的“偽DOM自定義事件”,。
四、偽DOM自定義事件
這里的“偽DOM自定義事件”是自己定義的一個名詞,,用來區(qū)分DOM自定義事件的,。例如jQuery庫,其是基于包裝器(一個包含DOM元素的中間層)擴展事件的,,既與DOM相關(guān),,又不直接是DOM,因此,,稱之為“偽DOM自定義事件”,。
//zxx: 下面即將展示的代碼目的在于學(xué)習與認識,要想實際應(yīng)用可能還需要在細節(jié)上做些調(diào)整,。例如,,下面測試的包裝器僅僅只是包裹DOM元素,并非選擇器之類,;$
符號未增加沖突處理,,且?guī)讉€重要方法都暴露在全局環(huán)境中,沒有閉包保護等,。
原型以及new
函數(shù)構(gòu)造不是本文重點,,因此,下面這個僅展示:
var $ = function(el) { return new _$(el); }; var _$ = function(el) { this.el = el; }; _$.prototype = { constructor: this, addEvent: function() { // ... }, fireEvent: function() { // ... }, removeEvent: function() { // ... } }
于是我們就可以使用類似$(dom).addEvent()
的語法為元素添加事件了(包括不包含瀏覽器行為的自定義事件)。
自定義事件的添加
如果只考慮事件添加,,我們的工作其實很簡單,,根據(jù)支持情況,addEventListener
與attachEvent
方法分別添加事件(attachEvent
方法后添加事件先觸發(fā))即可:
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; }
顯然,,事情不會這么簡單,,有句古話叫做“上山容易下山難”,自定義事件添加容易,,但是如何觸發(fā)它們呢,?——考慮到自定義事件與瀏覽器行為無關(guān),同時瀏覽器沒有直接的觸發(fā)事件的方法,。
自定義事件的觸發(fā)
又是不可避免的,,由于瀏覽器兼容性問題,我們要分開說了,,針對標準瀏覽器和IE6/7等考古瀏覽器,。
1. 對于標準瀏覽器,其提供了可供元素觸發(fā)的方法:element.dispatchEvent()
. 不過,,在使用該方法之前,,我們還需要做其他兩件事,及創(chuàng)建和初始化,。因此,,總結(jié)說來就是:
document.createEvent() event.initEvent() element.dispatchEvent()
舉個板栗:
$(dom).addEvent("alert", function() { alert("彈彈彈,彈走魚尾紋~~"); }); // 創(chuàng)建 var evt = document.createEvent("HTMLEvents"); // 初始化 evt.initEvent("alert", false, false); // 觸發(fā), 即彈出文字 dom.dispatchEvent(evt);
createEvent()
方法返回新創(chuàng)建的Event
對象,,支持一個參數(shù),,表示事件類型,具體見下表:
參數(shù) | 事件接口 | 初始化方法 |
---|---|---|
HTMLEvents | HTMLEvent | initEvent() |
MouseEvents | MouseEvent | initMouseEvent() |
UIEvents | UIEvent | initUIEvent() |
關(guān)于createEvent()
方法我自己了解也不是很深入,,不想濫竽充數(shù),,誤人子弟,所以您有疑問我可能作答不了,,希望對熟知該方法的人可以做進一步的解釋說明(例如事件接口與document
關(guān)系,,UIEvent
是什么東西等),。
initEvent()
方法用于初始化通過DocumentEvent
接口創(chuàng)建的Event
的值,。支持三個參數(shù):initEvent(eventName, canBubble, preventDefault)
. 分別表示事件名稱,是否可以冒泡,,是否阻止事件的默認操作,。
dispatchEvent()
就是觸發(fā)執(zhí)行了,dom.dispatchEvent(eventObject)
, 參數(shù)eventObject
表示事件對象,,是createEvent()
方法返回的創(chuàng)建的Event
對象,。
2. 對于IE瀏覽器,由于向下很多版本的瀏覽器都不支持document.createEvent()
方法,因此我們需要另辟蹊徑(據(jù)說IE有document.createEventObject()
和event.fireEvent()
方法,,但是不支持自定義事件~~),。
IE瀏覽器有不少自給自足的東西,例如下面要說的這個"propertychange"
事件,,顧名思意,,就是屬性改變即觸發(fā)的事件。例如文本框value
值改變,,或是元素id
改變,,或是綁定的事件改變等等。
我們可以利用這個IE私有的東西實現(xiàn)自定義事件的觸發(fā),,大家可以先花幾分鐘想想……
// zxx: 假設(shè)幾分鐘已經(jīng)過去了……
大家現(xiàn)在有思路了沒,?其實說穿了很簡單,當我們添加自定義事件的時候,,順便給元素添加一個自定義屬性即可,。例如,我們添加自定義名為"alert"
的自定義事件,,順便我們可以對元素做點小手腳:
dom.evtAlert = "2012-04-01";
再順便把自定義事件fn
塞到"propertychange"
事件中:
dom.attachEvent("onpropertychange", function(e) { if (e.propertyName == "evtAlert") { fn.call(this); } });
這個,,當我們需要觸發(fā)自定義事件的時候,只要修改DOM上自定義的evtAlert
屬性的值即可:
dom.evtAlert = Math.random(); // 值變成隨機數(shù)
此時就會觸發(fā)dom
上綁定的onpropertychange
事件,,又因為修改的屬性名正好是"evtAlert"
, 于是自定義的fn
就會被執(zhí)行,。這就是IE瀏覽器下事件觸發(fā)實現(xiàn)的完整機制,應(yīng)該說講得還是蠻細的,。
自定義事件的刪除
與觸發(fā)事件不同,,事件刪除,各個瀏覽器都提供了對于的時間刪除方法,,如removeEventListener
和detachEvent
,。不過呢,對于IE瀏覽器,,還要多刪除一個事件,,就是為了實現(xiàn)觸發(fā)功能額外增加的onpropertychange
事件:
dom.detachEvent("onpropertychange", evt);
大綜合
結(jié)合上面所有論述與展示,我們可以得到類似下面的完整代碼(為限制篇幅,,滾動定高,,想查看完整代碼推薦去原demo,或是點擊這里完整顯示– js交互,,RSS中無效果,。):
var $ = function(el) { return new _$(el); }; var _$ = function(el) { this.el = (el && el.nodeType == 1)? el: document; }; _$.prototype = { constructor: this, addEvent: function(type, fn, capture) { var el = this.el; if (window.addEventListener) { el.addEventListener(type, fn, capture); var ev = document.createEvent("HTMLEvents"); ev.initEvent(type, capture || false, false); if (!el["ev" + type]) { el["ev" + type] = ev; } } else if (window.attachEvent) { el.attachEvent("on" + type, fn); if (isNaN(el["cu" + type])) { // 自定義屬性 el["cu" + type] = 0; } var fnEv = function(event) { if (event.propertyName == "cu" + type) { fn.call(el); } }; el.attachEvent("onpropertychange", fnEv); if (!el["ev" + type]) { el["ev" + type] = [fnEv]; } else { el["ev" + type].push(fnEv); } } return this; }, fireEvent: function(type) { var el = this.el; if (typeof type === "string") { if (document.dispatchEvent) { if (el["ev" + type]) { el.dispatchEvent(el["ev" + type]); } } else if (document.attachEvent) { el["cu" + type]++; } } return this; }, removeEvent: function(type, fn, capture) { var el = this.el; if (window.removeEventListener) { el.removeEventListener(type, fn, capture || false); } else if (document.attachEvent) { el.detachEvent("on" + type, fn); var arrEv = el["ev" + type]; if (arrEv instanceof Array) { for (var i=0; i<arrEv.length; i+=1) { el.detachEvent("onpropertychange", arrEv[i]); } } } return this; } };
您可以狠狠地點擊這里:JS DOM自定義事件demo
demo頁面中的的張含韻小姐圖片上通過級聯(lián)形式聯(lián)系添加了三個事件(一個是包含瀏覽器行為的click
事件,還有兩個是自定義不含行為的alert
事件):
$(elImage) .addEvent("click", funClick); .addEvent("alert", funAlert1) .addEvent("alert", funAlert2);
而funClick
方法中有等同下面腳本:
$(e.target).fireEvent("alert");
因此,,點擊圖片,,才會出現(xiàn)三個彈出框:用戶點擊圖片 → 執(zhí)行funClick
→ 第一個彈框 → 執(zhí)行fireEvent
→ 觸發(fā)自定義"alert"
事件 → 連續(xù)兩個"alert"
事件彈框
當點擊圖片下面的按鈕清除掉自定義"alert"
事件后,,再點擊圖片就只有一個彈出咯(funAlert1
和funAlert2
提前回家掃墓去了)!
五,、清明節(jié)前的結(jié)語
明天回家,,很顯然,我要釣魚釣死在河邊上,。
時間等客觀原因,,本文展示的些腳本并未做非常詳盡嚴謹?shù)臏y試,因此,,不建議直接Copy到實際項目中應(yīng)用,,更多旨在相互交流與學(xué)習。例如在IE瀏覽器下,,最后的“偽DOM自定義事件”,,click事件通過點擊觸發(fā)時的事件類型(event.type
)是click
, 但是通過fireEvent
觸發(fā)的時候事件類型是propertychange
, 這些細節(jié)在測試學(xué)習的時候都是可以忽略的,但是要是實際應(yīng)用,,這都是需要完善的,。
本想以很通俗易懂的語言闡述我想表達的內(nèi)容,但是,,現(xiàn)在回過頭看看,,做得并不好,術(shù)語,,啰嗦的話語還是顯得多了點,,這方面的功力還需要加強,或許是本身理解不透徹的緣故,,無法駕馭自然無法語言通俗化,。
雖說自己JS方面的學(xué)習比兩年前要好多了(那個時候連addEventListener
和attachEvent
放在一起干嘛的都不清楚),但是心里清楚的很,,JS還是很薄弱的,,跟真正優(yōu)秀的JS開發(fā)人員相比,要積累的還有很多,。什么時候能夠像看有色小說一樣把《JavaScript語言精粹》一書讀下來,,恩,估計可以有臉得瑟得瑟了~~
本文涉及的一些知識點歡迎補充提點,,有表述不準確的地方歡迎指正,。
最后,祝大家清明節(jié)快樂,!額,?怎么這句話怪怪的——上墳一般快樂不起來吧~~那大家祝我清明回家釣魚大豐收,大爆箱??!哈哈!,!
原創(chuàng)文章,,轉(zhuǎn)載請注明來自張鑫旭-鑫空間-鑫生活[http://www.]
本文地址:http://www./wordpress/?p=2330