本篇開始將回顧下Javascript的事件機制。同時會從一個最小的函數開始寫到最後一個具備完整功能的,強大的事件模塊。爲敘述方便將響應函數/回調函數/事件Listener/事件handler都稱爲事件handler。 javascript
先看看頁面中添加事件的幾種方式: html
<div onclick="alert('歡迎訪問Nowamagic.net');">Nowamagic</div>
HTML Element元素自身就擁有了不少onXXX屬性,只需將JS代碼賦值給其就能夠了。賦值給onXXX的字符串將做爲響應函數的函數體(FunctionBody)。大概這是上世紀90年代的寫法,那時候直接把JS代碼寫在網頁中很廣泛,也許那時候的JS並不過重要,只是用來作作驗證或一些花哨的效果而已。 java
<script type="text/javascript"> function clk(){} </script> <div onclick="clk()">Div2 Element</div>
先定義函數clk,而後賦值給onclick屬性,這種方式也應該屬於上世紀90年代的流行寫法。比第一種方式好的是它把業務邏輯代碼都封裝在一個函數裏了,使HTML代碼與JS代碼稍微有點兒分離,不至於第一種那麼緊密耦合。 node
<div id="d3">Div3 Element</div> <script type="text/javascript"> var d3 = document.getElementById('d3'); d3.onclick = function(){ } </script>
這種方式也是早期的寫法,但好處是能夠將JS與HTML徹底分離,前提是須要給HTML元素提供一個額外的id屬性(或其它能獲取該元素對象的方式)。 數組
<div id="d4">Div4 Element</div> <script type="text/javascript"> var d4 = document.getElementById('d4'); function clk(){alert(4)} if(d4.addEventListener){ d4.addEventListener('click',clk,false); } if(d4.attachEvent){ d4.attachEvent('onclick',clk); } </script>
這是目前推薦的方式,較前兩種方式功能更爲強大,能夠爲元素添加多個事件handler,支持事件冒泡或捕獲,前三種方式默認都是冒泡。IE6/7/8仍然沒有遵循標準而使用了本身專有的attachEvent,且不支持事件捕獲。 瀏覽器
好,把方式4簡單的封裝下, 兼容標準瀏覽器及IE瀏覽器。注意attachEvent的第一個參數須要加上個"on",addEventListener第三個參數爲false表示事件冒泡,attachEvent沒有第三個參數,默認就是冒泡,沒有捕獲。 框架
/** * * @param {Object} el HTML Element * @param {Object} type 事件類型 * @param {Object} fn 事件handler */ function addEvent(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el.attachEvent('on' + type, fn); } }
好,用這個工具函數添加一個給document添加一個點擊事件: 函數
function handler(){ alert(this); alert(arguments[0]); } addEvent(document, 'click', handler);
在Firefox等標準瀏覽器中,點擊頁面後將彈出 "[object HTMLDocument]",及handler中的this就是document自身。但在IE6/7/8中this倒是window對象。這讓人不爽,修改下與標準瀏覽器統一。 工具
function addEvent(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e' + fn] = function(){ fn.call(el, window.event); } el.attachEvent('on'+type, el['e'+fn]); } }
上面咱們封裝了一個addEvent,解決了IE6/7/8下事件handler中this爲window的錯誤,而且統一了事件對象做爲事件handler的第一個參數傳入。 測試
這篇把對應的刪除事件的函數補上。上一篇中fn在IE6/7/8中實際上被包裝了,IE6/7/8中真正的handler是el["e"+fn]。所以刪除時要用到它。同時將兩個方法掛在一個對象E上,add,remove分別添加和刪除事件。
E = { //添加事件 add : function(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e'+fn] = function(){ fn.call(el,evt); }; el.attachEvent('on' + type, el['e'+fn]); } }, //刪除事件 remove : function(el, type, fn){ if(el.removeEventListener){ el.removeEventListener(type, fn, false); }else if(el.detachEvent){ el.detachEvent('on' + type, el['e'+fn]); } } };
能夠看到,標準瀏覽器如IE9/Firefox/Safari/Chrome/Opera會使用addEventListener/removeEventListener添加/刪除事件,IE6/7/8則使用attachEvent/detachEvent。標準瀏覽器中事件handler是傳入的第三個參數fn,IE6/7/8中則是包裝後的el["e"+fn]。
好了,已經擁有了添加,刪除事件兩個方法,而且解決了各瀏覽器下中的部分差別,現再添加一個主動觸發事件的方法dispatch。該方法能模擬用戶行爲,如點擊(click)操做等。 標準使用dispatchEvent方法,IE6/7/8則使用fireEvent方法。由於可能會出現異常,使用了try catch。
E = { //添加事件 add : function(el, type, fn){ if(el.addEventListener){ el.addEventListener(type, fn, false); }else{ el['e'+fn] = function(){ fn.call(el,window.event); }; el.attachEvent('on' + type, el['e'+fn]); } }, //刪除事件 remove : function(el, type, fn){ if(el.removeEventListener){ el.removeEventListener(type, fn, false); }else if(el.detachEvent){ el.detachEvent('on' + type, el['e'+fn]); } }, //主動觸發事件 dispatch : function(el ,type){ try{ if(el.dispatchEvent){ var evt = document.createEvent('Event'); evt.initEvent(type,true,true); el.dispatchEvent(evt); }else if(el.fireEvent){ el.fireEvent('on'+type); } }catch(e){}; } };
這就是整個事件模塊的雛形,日後還有不少須要補充完善的地方。但對於普通的應用,這幾個函數足以勝任。
上面的add有個問題,對同一類型事件添加多個hanlder時,IE6/7/8下會無序,如
<div id="d1" style="width:200px;height:200px;background:gold;"></div> <script type="text/javascript"> var el = document.getElementById('d1'); function handler1(){alert('1');} function handler2(){alert('2');} function handler3(){alert('3');} function handler4(){alert('4');} function handler5(){alert('5');} E.add(el, 'click', handler1); E.add(el, 'click', handler2); E.add(el, 'click', handler3); E.add(el, 'click', handler4); E.add(el, 'click', handler5); </script>
IE9/Firefox/Safari/Chomre/Opera會依次輸出1,2,3,4,5。但IE6/7/8中則不必定。爲解決全部瀏覽器中多個事件handler有序執行,咱們須要一個隊列來管理全部的handler。
此次,把全部的內部細節封裝在一個匿名函數中,該函數執行完畢後返回如上一篇接口相同的方法。另外
E = function(){ function _isEmptyObj(obj){ for(var a in obj){ return false; } return true; } function _each(ary, callback){ for(var i=0,len=ary.length; i<len;){ callback(i, ary[i]) ? i=0 : i++; } } function _remove(el, type){ var handler = el.listeners[type]['_handler_']; el.removeEventListener ? el.removeEventListener(type, handler, false) : el.detachEvent('on'+type, handler); delete el.listeners[type]; if(_isEmptyObj(el.listeners)){ delete el.listeners; } } // 添加事件 function add(el, type, fn){ el.listeners = el.listeners || {}; var listeners = el.listeners[type] = el.listeners[type] || []; listeners.push(fn); if(!listeners['_handler_']){ listeners['_handler_'] = function(e){ var evt = e || window.event; for(var i=0,fn; fn=listeners[i++];){ fn.call(el, evt); } } el.addEventListener ? el.addEventListener(type, listeners['_handler_'], false) : el.attachEvent('on' + type, listeners['_handler_']); } } // 刪除事件 function remove(el, type, fn){ if(!el.listeners) return; var listeners = el.listeners && el.listeners[type]; if(listeners) { _each(listeners, function(i, f){ if(f==fn){ return listeners.splice(i, 1); } }); if(listeners.length == 0){ _remove(el,type); } } } //主動觸發事件 function dispatch(el ,type){ try{ if(el.dispatchEvent){ var evt = document.createEvent('Event'); evt.initEvent(type,true,true); el.dispatchEvent(evt); }else if(el.fireEvent){ el.fireEvent('on'+type); } }catch(e){}; } return { add: add, remove: remove, dispatch: dispatch }; }();
上面解決了IE6/7/8中同一個類型事件的多個handler執行無序的狀況,爲此改動也是較大的。實現幾乎與前一個版本徹底不一樣。但好處也是明顯的。
有時須要添加只執行一次的事件handler,爲此給add方法添加第四個參數one,one爲true則該事件handler只執行一次。
<div id="d1" style="width:200px;height:200px;background:gold;"></div> <script> var el = document.getElementById('d1'); function handler(){alert(5)} E.add(el, 'click', handler, true); </script>
再擴展下remove函數。
好比當給一個el添加了3個click事件的handler,1個mouseover事件的handler
function handler1(){alert('1');} function handler2(){alert('2');} function handler3(){alert('3');} function handler4(){alert('4');} E.add(el, 'click', f1); E.add(el, 'click', f2); E.add(el, 'click', f3); E.add(el, 'mouseover', f4);
使用如下語句將刪除元素click的全部handler:E.remove(el, 'click');
如下將刪除元素身上全部的事件handler,包括click和mouseover:E.remove(el);
上面正式推出了個人事件模塊event_v1,已經搭起了它的初始框架。或許有人要說,與衆多JS庫或框架相比,它尚未解決事件對象的兼容性問題。是的,我故意將此放到後續補充。由於事件對象的兼容性問題太多了,太繁瑣了。
下面我將引入一個私有的_fixEvent函數,add中將調用該函數。_fixEvent將修復(或稱包裝)原生事件對象,返回一個標準的統一接口的事件對象。以下
function _fixEvent( evt, el ) { var props = "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), len = props.length; function now() {return (new Date).getTime();} function returnFalse() {return false;} function returnTrue() {return true;} function Event( src ) { this.originalEvent = src; this.type = src.type; this.timeStamp = now(); } Event.prototype = { preventDefault: function() { this.isDefaultPrevented = returnTrue; var e = this.originalEvent; if( e.preventDefault ) { e.preventDefault(); } e.returnValue = false; }, stopPropagation: function() { this.isPropagationStopped = returnTrue; var e = this.originalEvent; if( e.stopPropagation ) { e.stopPropagation(); } e.cancelBubble = true; }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); }, isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse }; var originalEvent = evt; evt = new Event( originalEvent ); for(var i = len, prop; i;) { prop = props[ --i ]; evt[ prop ] = originalEvent[ prop ]; } if(!evt.target) { evt.target = evt.srcElement || document; } if( evt.target.nodeType === 3 ) { evt.target = evt.target.parentNode; } if( !evt.relatedTarget && evt.fromElement ) { evt.relatedTarget = evt.fromElement === evt.target ? evt.toElement : evt.fromElement; } if( evt.pageX == null && evt.clientX != null ) { var doc = document.documentElement, body = document.body; evt.pageX = evt.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); evt.pageY = evt.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); } if( !evt.which && ((evt.charCode || evt.charCode === 0) ? evt.charCode : evt.keyCode) ) { evt.which = evt.charCode || evt.keyCode; } if( !evt.metaKey && evt.ctrlKey ) { evt.metaKey = evt.ctrlKey; } if( !evt.which && evt.button !== undefined ) { evt.which = (evt.button & 1 ? 1 : ( evt.button & 2 ? 3 : ( evt.button & 4 ? 2 : 0 ) )); } if(!evt.currentTarget) evt.currentTarget = el; return evt; }
好了,如今你要
更多的差別性,不在這一一列舉了。