JavaScript 是基於面向對象和事件驅動的一門語言,事件模型是 DOM 中至關重要的內容,理解事件驅動機制、事件反饋、事件冒泡、事件捕獲以及事件委托能幫助我們更好的處理事件,寫出更優的代碼 ...
JavaScript DOM 事件模型
JavaScript 是基於面向對象和事件驅動的一門語言,事件模型是 DOM 中至關重要的內容,理解事件驅動機制、事件反饋、事件冒泡、事件捕獲以及事件委托能幫助我們更好的處理事件,寫出更優的代碼
事件驅動機制
- 當事件發生時,我們收到事件的反饋,在 JavaScript 中,事件反饋是我們自行定義的事件處理函數
- 事件,如點擊事件、滑鼠移入事件等,是每一個元素與生俱來的能力
- 通常說的綁定事件,實際上是綁定事件的反饋,即事件處理函數
- 例如點擊一個按鈕,按鈕元素對象是事件發送器或事件源,事件是滑鼠點擊事件,事件處理函數是偵聽器
- 元素對象發出事件,事件處理函數做出反應,這就是 JS 的事件驅動機制
在觀察者模式中,事件發送器就是主題,事件處理函數即偵聽器就是觀察者
綁定事件反饋
-
內聯屬性
<button onclick="test()">按鈕</button>
介於結構和邏輯要相分離,不建議使用內聯方式綁定
-
事件句柄
var oBtn = document.getElementsByTagName('button')[0]; oBtn.onclick = function() { // this -> oBtn }
相容性好,但是重覆綁定會覆蓋
-
事件監聽器
var oBtn = document.getElementsByTagName('button')[0]; oBtn.addEventListener("click", funtion(){ // this -> oBtn }, false); oBtn.addEventListener("click", test, false); funtion test(){ // 事件處理函數 }
重覆添加,不會覆蓋之前添加的監聽器,但是如果事件類型、事件處理函數和最後一個布爾參數都相同,則不會重覆執行
IE8 及以下不支持 addEventListener,可用 attachEvent 代替
var oBtn = document.getElementsByTagName('button')[0]; oBtn.attachEvent("onclick", funtion(){ // this -> window }); // 區別於 addEventListener,第一個參數使用 'onclick',而不是 'click' // 並且內部 this 指向 window // 對於 attachEvent,如果事件類型、事件處理函數都相同,還是會重覆執行
相容性封裝
function addEvent(elem, type, fn) { if (elem.addEventListener) { elem.addEventListener(type, fn, false); } else if (elem.attachEvent) { elem.attachEvent('on' + type, function(ev) { fn.call(elem, ev); // call 相容性比 bind 好 }); } else { elem['on' + type] = fn; } }
-
解除綁定
oBtn.onclik = null; oBtn.removeEventListener("click", test, false); // 解除 addEventListener oBtn.detachEvent('onclick', test); // 解除 attachEvent
示例:點擊一次後清除事件反饋
oBtn.onclik = function() { // ... this.onclick = null; } // 非嚴格模式 oBtn.addEventListener("click", funtion() { // ... this.removEventListener('cilck', arguments.callee, false); }, false); // 嚴格模式 oBtn.addEventListener("click", funtion temp() { // ... this.removeEventListener('click', temp, false); }, false);
事件冒泡和捕獲
-
事件冒泡:當一個元素髮生事件時,該事件會向父級元素傳遞,按由子到父的順序觸發一連串的事件反饋,稱之為事件冒泡
DOM 上的嵌套關係會產生事件冒泡,例如兩個 div 嵌套,點擊內部的 div,觸發內部 div 的點擊事件,內部 div 的點擊事件處理函數進行響應,這個事件向其父級即外部 div 傳遞,外部 div 也有點擊事件,外部 div 所綁定的點擊事件反饋也會響應
<div class="outer"> <div class="inner"></div> </div>
var outer = document.getElementsByClassName('outer')[0], inner = outer.getElementsByClassName('inner')[0]; outer.addEventListener('click', function () { console.log('bubble outer'); }, false); inner.addEventListener('click', function () { console.log('bubble inner'); }, false); // addEventListener 最後一個參數預設值為 false,表示事件冒泡 // 點擊 inner,列印出 // bubble inner // bubble outer
-
事件捕獲:當一個元素髮生事件時,該事件會向父級元素傳遞,按由父到子的順序觸發一連串的事件反饋,稱之為事件捕獲
事件捕獲與事件冒泡的觸發順序相反,同樣需要 DOM 上的嵌套關係
outer.addEventListener('click', function () { console.log('outer'); }, true); inner.addEventListener('click', function () { console.log('inner'); }, true); // addEventListener 最後一個參數使用 true,表示事件捕獲 // 點擊 inner,列印出 // outer // in
-
捕獲和冒泡的執行順序
outer.addEventListener('click', function () { console.log('bubble outer'); }, false); // 冒泡 inner.addEventListener('click', function () { console.log('bubble inner'); }, false); // 冒泡 outer.addEventListener('click', function () { console.log('outer'); }, true); // 捕獲 inner.addEventListener('click', function () { console.log('inner'); }, true); // 捕獲 // 點擊 inner,列印出 // outer // bubble inner // inner // bubble outer
點擊一個元素,元素即事件源,若事件源綁定了事件處理函數,且設定了事件捕獲,則先執行捕獲,捕獲執行完畢後,按照綁定順序執行該事件源綁定的事件,如果設定了事件冒泡,再執行冒泡
-
focus blur change submit reset select 事件沒有冒泡和捕獲,IE 瀏覽器沒有事件捕獲
阻止事件冒泡
-
阻止冒泡的方法
Event 的原型上有 stopPropagation 方法,可以阻止冒泡,是 w3c 的規範
Event 的原型上有 cancleBubble 屬性,賦值為 true,可以阻止冒泡
-
addEventListener 綁定事件處理函數,拿到事件對象
var outer = document.getElementsByClassName('outer')[0], inner = outer.getElementsByClassName('inner')[0]; inner.addEventListener('click', function (ev) { console.log(ev); // 事件對象 ev ev.stopPropagation(); // 阻止事件冒泡 }, false);
-
IE 瀏覽器沒有 stopPropagation 方法,可以使用 cancelBubble 屬性
註意:IE 瀏覽器中事件對象存放在 window.event 中。IE8 不支持 addEventListener 方法
// 封裝阻止冒泡的方法 function cancelBubble(ev) { if (ev.stopPropagation) { ev.stopPropagation(); } else ev.cancelBubble = true; // 相容 IE8 及以下 } // 使用上文中封裝好的 addEvent 方法 function addEvent(elem, type, fn) { if (elem.addEventListener) { elem.addEventListener(type, fn); } else if (elem.attachEvent) { elem.attachEvent('on' + type, function (ev) { fn.call(elem, ev); }); } else { elem['on' + type] = fn; } } // 綁定事件處理函數 var outer = document.getElementsByClassName('outer')[0], inner = outer.getElementsByClassName('inner')[0]; addEvent(inner, 'click', function (ev) { var ev = ev || window.event; // IE 相容性寫法 cancelBubble(ev); // 阻止冒泡 });
阻止預設事件
-
三種方法
- 事件對象 preventDefault() 方法,相容 IE9 及以上
- 事件對象 returnValue = false,相容 IE8 及以下
- 事件處理函數 return false
-
相容性寫法
function preventDefaultEvent(ev) { if (ev.preventDefault) { ev.preventDefault(); } else ev.returnValue = false; // 相容 IE8 及以下 }
-
右鍵菜單事件
document.oncontextmenu = function (ev) { var ev = ev || window.event; // 1. ev.preventDefault(); // IE9 及以上 // 2. ev.returnValue = false; // IE8 及以下 // 3. return false; }
-
a 標簽跳轉事件
href 使用偽協議
<a href="javascript:void(0);">a 標簽</a> <a href="javascript:;">a 標簽</a> <a href="#">a 標簽</a> <!--跳轉到當前頁面頂部-->
onclick 事件 return false
<a href="http://www.baidu.com" onclick="return false">a 標簽</a> <a href="http://www.baidu.com" onclick="return test(),false">a 標簽</a> <!--第二個是利用了 “,” 分隔符會返回最後一個的特點,與 test 方法無關-->
綁定事件處理函數
<!--內聯綁定--> <a id='taga' href="http://www.baidu.com" onclick="return test()">a 標簽</a> <!--句柄綁定--> <script> document.getElementById('taga').onclick = test; function test(ev) { var ev = ev || window.event; // 1. ev.preventDefault(); // IE9 及以上 // 2. ev.returnValue = false; // IE8 及以下 // 3. return false; } // 前兩種方式在使用內聯屬性綁定時,不需要在屬性上加 return,第三種則需要 </script>
表單的 action 屬性支持
javascript:
偽協議,onsubmit 或者提交按鈕點擊事件都可以綁定處理函數,阻止提交的方法和阻止 a 標簽跳轉的方法類似
冒泡捕獲流
-
事件流:描述從頁面中接收事件的順序
-
事件冒泡流:微軟 IE 提出,Event Bubbling
-
事件捕獲流:網景 Netscape 提出,Event Capturing
-
事件流三個階段:事件捕獲階段、處於目標階段、事件冒泡階段
元素觸發事件時,首先事件捕獲階段,由父到子的執行事件處理函數,然後處於目標階段,該元素的事件處理函數按綁定順序執行,最後事件冒泡階段,由子到父的執行事件處理函數
事件和事件源
-
事件即事件對象,可以由事件處理函數的參數拿到
IE8 及以下中事件對象存放在 window.event 中
// btn 按鈕元素 btn.onclick = function(ev) { var ev = ev || window.event; // IE8 相容性寫法 }
-
事件源即事件源對象,是發生事件的元素,即事件發送器,可以從事件對象中獲取
IE8 及以下只有 srcElement,firefox 低版本只有 target,chrome 兩者都有
// btn 按鈕元素 btn.onclick = function(ev) { var ev = ev || window.event; // IE8 相容性寫法 var tar = ev.target || ev.srcElement; // 獲取事件源的相容性寫法 }
事件委托
-
事件委托也叫事件代理,指對父級元素綁定事件處理函數,通過獲取事件源來處理子元素
-
示例:點擊按鈕使列表 ul 增加 li 元素,點擊每個 li 元素列印出其中的內容(innerHTML)
如果不使用事件委托,需要迴圈對每個 li 進行綁定,點擊按鈕添加新的 li 元素後也要進行綁定,效率低下
使用事件委托,直接對 ul 綁定點擊事件處理函數,獲取事件對象、事件源對象,再對源對象進行處理
<body> <button>btn</button> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> </ul> <script> var oBtn = document.getElementsByTagName('button')[0], oList = document.getElementsByTagName('ul')[0], oLi = oList.getElementsByTagName('li'); oBtn.onclick = function () { var li = document.createElement('li'); li.innerText = oLi.length + 1; oList.appendChild(li); } oList.onclick = function (ev) { var ev = ev || window.event, tar = ev.target || ev.srcElement; // tar 即為被點擊的 li 元素 console.log(tar.innerHTML); // 返回在所有兄弟元素中的索引,借用數組 indexOf 方法 console.log(Array.prototype.indexOf.call(oLi, tar)); } </script> </body>