深入淺出js事件
一.事件流
事件冒泡和事件捕獲分別由微軟和網景公司提出,這兩個概念是為了解決頁面中事件流(事件發生順序)的問題。
<div id="outer"> <p id="inner">Click me!</p> </div>
上面的代碼當中一個div元素當中有一個p子元素,如果兩個元素都有一個click的處理函數,那么我們怎么才能知道哪一個函數會首先被觸發呢?
為了解決這個問題微軟和網景提出了兩種幾乎完全相反的概念。
1.事件冒泡
微軟提出了名為事件冒泡的事件流。事件冒泡可以形象地比喻為把一顆石頭投入水中,泡泡會一直從水底冒出水面。也就是說,事件會從最內層的元素開始發生,一直向上傳播,直到document對象。
因此上面的例子在事件冒泡的概念下發生click事件的順序應該是p -> div -> body -> html -> document
2.事件捕獲
網景提出另一種事件流名為事件捕獲與事件冒泡相反,事件會從最外層開始發生,直到最具體的元素。
上面的例子在事件捕獲的概念下發生click事件的順序應該是document -> html -> body -> div -> p
3.W3C事件階段(event phase):
當一個DOM事件被觸發的時候,他並不是只在它的起源對象上觸發一次,而是會經歷三個不同的階段。簡而言之:事件一開始從文檔的根節點流向目標對象(捕獲階段),然后在目標對向上被觸發(目標階段),之后再回溯到文檔的根節點(冒泡階段)如圖所示(圖片來自W3C):
事件捕獲階段(Capture Phase)
事件從文檔的根節點出發,隨着DOM樹的結構向事件的目標節點流去。途中經過各個層次的DOM節點,並在各節點上觸發捕獲事件,直到到達時間的目標節點。捕獲階段的主要任務是簡歷傳播路徑,在冒泡階段,時間會通過這個路徑回溯到文檔根節點。
例如,通過下面的這個函數來給節點設置監聽,可以通過將;設置成true來為事件的捕獲階段添加監聽回調函數。
element.removeEventListener(<event-name>, <callback>, <use-capture>);
而,在實際應用中,我們並沒有太多使用捕獲階段監聽的用例,但是通過在捕獲階段對事件的處理,我們可以阻止類似click事件在某個特定元素上被觸發。如下:
var form=document.querySeletor('form'); form.addEventListener('click',function(e){ e.stopPropagation(); },true);
如果你對這種用法不是很了解的話,建議設置為false或者undefined,從而在冒泡階段對事件進行監聽,這也是常用的方法。
目標階段(Target Phase)
當事件到達目標節點時,事件就進入了目標階段。事件在目標節點上被觸發,然后逆向回流,知道傳播到最外層的文檔節點。
對於多層嵌套的節點,鼠標和指針事件經常會被定位到最里層的元素上。假設,你在一個div元素上設置了click的監聽函數,而用戶點擊在了這個div元素內部的p元素上,那么p元素就是這個時間的目標元素。事件冒泡讓我們可以在這個div或者更上層的元素上監聽click事件,並且時間傳播過程中觸發回調函數。
冒泡階段(Bubble Phase)
事件在目標事件上觸發后,並不在這個元素上終止。它會隨着DOM樹一層層向上冒泡,直到到達最外層的根節點,一直向上傳播,直到document對象。也就是說,同一事件會一次在目標節點的父節點,父節點的父節點...直到最外層的節點上觸發。
絕大多數事件是會冒泡的,但並非所有的。
二.事件代理(event delegation)
在JavaScript中,經常會碰到要監聽列表中多項li的情形,假設我們有一個列表如下:
<ul id="list"> <li id="item1">item1</li> <li id="item2">item2</li> <li id="item3">item3</li> <li id="item4">item4</li> </ul>
如果我們要實現以下功能:當鼠標點擊某一li時,alert輸出該li的內容,我們通常的寫法是這樣的:
-當列表項比較少時,直接在html里給每個li添加onclick事件
-列表項比較多時,在onload時就給每個列表項調用監聽
第一種方法比較簡單直接,但是沒有顧及到html與JavaScript的分離,不建議使用,第二種方法的代碼如下:
window.onload=function(){ var ulNode=document.getElementById("list"); var liNodes=ulNode.childNodes||ulNode.children; for(var i=0;i<liNodes.length;i++){ liNodes[i].addEventListener('click',function(e){ alert(e.target.innerHTML); },false); } }
由上可以看出來,假如不停的刪除或添加li,則function()也要不停的更改操作,易出錯,因此推薦使用事件代理。
在傳統的事件處理中,你按照需要為每一個元素添加或者是刪除事件處理器。然而,事件處理器將有可能導致內存泄露或者是性能下降——你用得越多這種風險就越大。JavaScript事件代理則是一種簡單的技巧,通過它你可以把事件處理器添加到一個父級元素上,這樣就避免了把事件處理器添加到多個子級元素上。
事件代理機制
事件代理用到了兩個在JavaSciprt事件中兩個特性:事件冒泡以及目標元素。使用事件代理,我們可以把事件處理器添加到一個元素上,等待一個事件從它的子級元素里冒泡上來,並且可以得知這個事件是從哪個元素開始的。
事件代理實現
例如,有一個table元素,ID是“report”,我們為這個表格添加一個事件處理器以調用editCell函數。
第一步,找到目標元素
editCell函數需要判斷傳到table來的事件的目標元素。考慮到我們要寫的幾個函數中都有可能用到這一功能,所以我們把它單獨放到一個名為getEventTarget的函數中:
function getEventTarget(e) { e = e || window.event;//事件對象 return e.target || e.srcElement; }
在IE里目標元素放在srcElemnt屬性中,而在其它瀏覽器里則是target屬性。
第二步,判斷目標元素,進行相關操作
接下來就是editCell函數了,這個函數調用到了getEventTarget函數。一旦我們得到了目標元素,剩下的事情就是看看它是否是我們所需要的那個元素了。
function editCell(e){ var target = getEventTarget(e); if(target.tagName.toLowerCase() =='td') { // DO SOMETHING WITH THE CELL } }
由上敘述,我們可以使用事件代理來實現本小結開始對每一個li的監聽。第三種方法,代碼如下:
window.onload=function(){ e = e || window.event; target = e.target || e.srcElement; var ulNode=document.getElementById("list"); ulNode.addEventListener('click',function(e){ if(target&&target.nodeName.toUpperCase()=="LI"){/*判斷目標事件是否為li*/ alert(e.target.innerHTML); } },false); };
注:
-tagName 屬性返回元素的標簽名。在 HTML 中,tagName 屬性的返回值始終是大寫的。
-nodeName 屬性指定節點的節點名稱。如果節點是元素節點,則 nodeName 屬性返回標簽名。如果節點是屬性節點,則 nodeName 屬性返回屬性的名稱。對於其他節點類型,nodeName 屬性返回不同節點類型的不同名稱。試一試。
三.事件兼容處理
現代綁定中W3C 使用的是:addEventListener 和removeEventListener。IE 使用的是attachEvent 和detachEvent。我們知道IE 的這兩個問題多多,並且伴隨內存泄漏。所以,解決這些問題非常有必要。
那么我們希望解決非IE 瀏覽器事件綁定哪些問題呢?
1)支持同一元素的同一事件句柄可以綁定多個監聽函數;
2)如果在同一元素的同一事件句柄上多次注冊同一函數,那么第一次注冊后的所有注冊
都被忽略;
3)函數體內的this 指向的應當是正在處理事件的節點(如當前正在運行事件句柄的節
點);
4)監聽函數的執行順序應當是按照綁定的順序執行;
5)在函數體內不用使用event = event || window.event; 來標准化Event 對象;
設計原理
1.通過使用傳統事件綁定對IE 進行封裝,模擬現代事件綁定(不使用attachEvent/detachEvent)。
2.把IE常用的Event對象配對到W3C中去
//跨瀏覽器添加事件綁定 function addEvent(obj, type, fn) { if (typeof obj.addEventListener != 'undefined') { obj.addEventListener(type, fn, false); } else { //創建一個存放事件的哈希表(散列表) if (!obj.events) obj.events = {}; //第一次執行時執行 if (!obj.events[type]) { //創建一個存放事件處理函數的數組 obj.events[type] = []; //把第一次的事件處理函數先儲存到第一個位置上 if (obj['on' + type]) obj.events[type][0] = fn; } else { //同一個注冊函數進行屏蔽,不添加到計數器中 if (addEvent.equal(obj.events[type], fn)) return false; } //從第二次開始我們用事件計數器來存儲 obj.events[type][addEvent.ID++] = fn; //執行事件處理函數 obj['on' + type] = addEvent.exec; } } //為每個事件分配一個計數器 addEvent.ID = 1; //執行事件處理函數 addEvent.exec = function (event) { var e = event || addEvent.fixEvent(window.event); var es = this.events[e.type]; for (var i in es) { es[i].call(this, e); } }; //同一個注冊函數進行屏蔽 addEvent.equal = function (es, fn) { for (var i in es) { if (es[i] == fn) return true; } return false; } //把IE常用的Event對象配對到W3C中去 addEvent.fixEvent = function (event) { event.preventDefault = addEvent.fixEvent.preventDefault; event.stopPropagation = addEvent.fixEvent.stopPropagation; event.target = event.srcElement; return event; }; //IE阻止默認行為 addEvent.fixEvent.preventDefault = function () { this.returnValue = false; }; //IE取消冒泡 addEvent.fixEvent.stopPropagation = function () { this.cancelBubble = true; }; //跨瀏覽器刪除事件 function removeEvent(obj, type, fn) { if (typeof obj.removeEventListener != 'undefined') { obj.removeEventListener(type, fn, false); } else { if (obj.events) { for (var i in obj.events[type]) { if (obj.events[type][i] == fn) { delete obj.events[type][i]; } } } } }
addEvent的優點
-可以在所有瀏覽器中工作,就算是更古老無任何支持的瀏覽器
-this關鍵字可以在所有的綁定函數中使用,指向的是當前元素
-中和了所有防止瀏覽器默認行為和阻止事件冒泡的各種瀏覽器特定函數
-不管瀏覽器類型,事件對象總是作為第一個對象傳入
addEvent的缺點
-僅工作在冒泡階段(因為它深入使用事件綁定的傳統方式)
知識說明:
添加、移除事件,代碼如下:
var addEvent = document.addEventListener ? function(elem,type, listener, useCapture) { elem.addEventListener(type, listener, useCapture); }: function(elem, type, listener, useCapture) { elem.attachEvent('on' + type, listener); }; var delEvent = document.removeEventListener ? function(elem, type, listener, useCapture) { elem.removeEventListener(type, listener, useCapture) }: function(elem, type, listener, useCapture) { elem.detachEvent('on' + type, listener); };
阻止事件繼續傳播,代碼如下:
function stopEvent (evt) { var evt = evt || window.event; if (evt.stopPropagation) { evt.stopPropagation(); } else { evt.cancelBubble = true; } }
取消默認行為,代碼如下:
function stopEvent (evt) { var evt = evt || window.event; if (evt.preventDefault) { evt.preventDefault(); } else { evt.returnValue = false; } }
四.事件類型
事件類型有:UI(用戶界面)事件,用戶與頁面上元素交互時觸發 ;焦點事件:當元素獲得或失去焦點時觸發 ; 文本事件:當在文檔中輸入文本時觸發;鍵盤事件:當用戶通過鍵盤在頁面上執行操作時觸發;鼠標事件:當用戶通過鼠標在頁面上執行操作時觸發;滾輪事件:當使用鼠標滾輪(或類似設備)時觸發。它們之間是繼承的關系,如下圖:
1.
常用:window、image。例如設置默認的圖片:
<img src="photo" alt="photo.jpg" onerror="this.src='defualt.jpg'">
2.
3.
4.
5.
6.
MouseEvent 順序
從元素A上方移過
-mousemove-> mouseover (A) ->mouseenter (A)-> mousemove (A) ->mouseout (A) ->mouseleave (A)
點擊元素
-mousedown-> [mousemove]-> mouseup->click
7.
詳細使用,請參考js事件手冊。如:http://www.w3school.com.cn/jsref/dom_obj_event.asp
-------------------------------------------------------------------------------------------------------------------------------------
完
轉載需注明轉載字樣,標注原作者和原博文地址。
更多閱讀:
http://blog.sina.com.cn/s/blog_5f54f0be0100cy49.html
http://www.cnblogs.com/luhangnote/archive/2012/08/16/2642657.html





![268737333e2e44c4b6de2a14618409c9[4] 268737333e2e44c4b6de2a14618409c9[4]](/image/aHR0cHM6Ly9pbWFnZXMwLmNuYmxvZ3MuY29tL2Jsb2cvNzA3MDUwLzIwMTUwNy8xODE0NDMwMzA0ODk5NjAuanBn.png)



