深入理解DOM事件机制
前言
本文主要介紹:
一、DOM事件級別
針對不同級別的DOM,我們的DOM事件處理方式也是不一樣的。
DOM級別一共可以分為4個級別:DOM0級「通常把DOM1規范形成之前的叫做DOM0級」,DOM1級,DOM2級和 DOM3級,而DOM事件分為3個級別:DOM0級事件處理,DOM2級事件處理和DOM3級事件處理。如下圖所示:
1.DOM 0級事件
在了解DOM0級事件之前,我們有必要先了解下HTML事件處理程序,也是最早的這一種的事件處理方式,代碼如下:
<button type="button" onclick="fn" id="btn">點我試試</button><script>function fn() {alert('Hello World');} </script> 復制代碼那有一個問題來了,那就是fn要不要加括號呢?
在html的onclick屬性中,使用時要加括號,在js的onclick中,給點擊事件賦值,不加括號。為什么呢?我們通過事實來說話:
// fn不加括號 <button type="button" onclick="fn" id="btn">點我試試</button><script>function fn() {alert('Hello World');}console.log(document.getElementById('btn').onclick);// 打印的結果如下:這個函數里面包括著fn,點擊之后并沒有彈出1/*? onclick(event) {fn}*/ </script>// fn 加括號,這里就不重復寫上面代碼,只需要修改一下上面即可 <button type="button" onclick="fn()" id="btn">點我試試</button> <script> // 打印的結果如下:點擊之后可以彈出1 /* ? onclick(event) {fn() } */ </script> 復制代碼上面的代碼我們通過直接在HTML代碼當中定義了一個onclick的屬性觸發fn方法,這樣的事件處理程序最大的缺點就是HTML與JS強耦合,當我們一旦需要修改函數名就得修改兩個地方。當然其優點就是不需要操作DOM來完成事件的綁定。
DOM0事件綁定,給元素的事件行為綁定方法,這些方法都是在當前元素事件行為的冒泡階段(或者目標階段)執行的。
那我們如何實現HTML與JS低耦合?這樣就有DOM0級處理事件的出現解決這個問題。DOM0級事件就是將一個函數賦值給一個事件處理屬性,比如:
<button id="btn" type="button"></button><script>var btn = document.getElementById('btn');btn.onclick = function() {alert('Hello World');}// btn.onclick = null; 解綁事件 </script> 復制代碼上面的代碼我們給button定義了一個id,然后通過JS獲取到了這個id的按鈕,并將一個函數賦值給了一個事件處理屬性onclick,這樣的方法便是DOM0級處理事件的體現。我們可以通過給事件處理屬性賦值null來解綁事件。DOM 0級的事件處理的步驟:先找到DOM節點,然后把處理函數賦值給該節點對象的事件屬性。
DOM0級事件處理程序的缺點在于一個處理程序「事件」無法同時綁定多個處理函數,比如我還想在按鈕點擊事件上加上另外一個函數。
var btn = document.getElementById('btn');btn.onclick = function() {alert('Hello World'); } btn.onclick = function() {alert('沒想到吧,我執行了,哈哈哈'); } 復制代碼2.DOM2級事件
DOM2級事件在DOM0級事件的基礎上彌補了一個處理程序無法同時綁定多個處理函數的缺點,允許給一個處理程序添加多個處理函數。也就是說,使用DOM2事件可以隨意添加多個處理函數,移除DOM2事件要用removeEventListener。代碼如下:
<button type="button" id="btn">點我試試</button><script>var btn = document.getElementById('btn');function fn() {alert('Hello World');}btn.addEventListener('click', fn, false);// 解綁事件,代碼如下// btn.removeEventListener('click', fn, false); </script> 復制代碼DOM2級事件定義了addEventListener和removeEventListener兩個方法,分別用來綁定和解綁事件
target.addEventListener(type, listener[, useCapture]); target.removeEventListener(type, listener[, useCapture]); /*方法中包含3個參數,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數、是否在捕獲時執行事件處理函數(關于事件冒泡和事件捕獲下面會介紹) */ 復制代碼注:
IE8級以下版本不支持addEventListener和removeEventListener,需要用attachEvent和detachEvent來實現:
// IE8級以下版本只支持冒泡型事件,不支持事件捕獲所以沒有第三個參數 // 方法中包含2個參數,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數 btn.attachEvent('onclick', fn); // 綁定事件 btn.detachEvent('onclick', fn); // 解綁事件 復制代碼3.DOM3級事件
DOM3級事件在DOM2級事件的基礎上添加了更多的事件類型,全部類型如下:
同時DOM3級事件也允許使用者自定義一些事件。
DOM事件級別的發展使得事件處理更加完整豐富,而下一個問題就是之前提到的DOM事件模型。「事件冒泡和事件捕獲」
二、DOM事件流
為什么是有事件流?
假如在一個button上注冊了一個click事件,又在其它父元素div上注冊了一個click事件,那么當我們點擊button,是先觸發父元素上的事件,還是button上的事件呢,這就需要一種約定去規范事件的執行順序,就是事件執行的流程。
瀏覽器在發展的過程中出現了兩種不同的規范
- IE9以下的IE瀏覽器使用的是事件冒泡,先從具體的接收元素,然后逐步向上傳播到不具體的元素。
- Netscapte采用的是事件捕獲,先由不具體的元素接收事件,最具體的節點最后才接收到事件。
- 而W3C制定的Web標準中,是同時采用了兩種方案,事件捕獲和事件冒泡都可以。
三、DOM事件模型
DOM事件模型分為捕獲和冒泡。一個事件發生后,會在子元素和父元素之間傳播(propagation)。這種傳播分成三個階段。
(1)捕獲階段:事件從window對象自上而下向目標節點傳播的階段;
(2)目標階段:真正的目標節點正在處理事件的階段;
(3)冒泡階段:事件從目標節點自下而上向window對象傳播的階段。
上文中講到了addEventListener的第三個參數為指定事件是否在捕獲或冒泡階段執行,設置為true表示事件在捕獲階段執行,而設置為false表示事件在冒泡階段執行。那么什么是事件冒泡和事件捕獲呢?可以用下圖來解釋:
1.事件捕獲
捕獲是從上到下,事件先從window對象,然后再到document(對象),然后是html標簽(通過document.documentElement獲取html標簽),然后是body標簽(通過document.body獲取body標簽),然后按照普通的html結構一層一層往下傳,最后到達目標元素。我們只需要將addEventListener的第三個參數改為true就可以實現事件捕獲。代碼如下:
<!-- CSS 代碼 --> <style>body{margin: 0;}div{border: 1px solid #000;}#grandfather1{width: 200px;height: 200px;}#parent1{width: 100px;height: 100px;margin: 0 auto;}#child1{width: 50px;height: 50px;margin: 0 auto;} </style><!-- HTML 代碼 --> <div id="grandfather1">爺爺<div id="parent1">父親<div id="child1">兒子</div></div> </div><!-- JS 代碼 --> <script>var grandfather1 = document.getElementById('grandfather1'),parent1 = document.getElementById('parent1'),child1 = document.getElementById('child1');grandfather1.addEventListener('click',function fn1(){console.log('爺爺');},true)parent1.addEventListener('click',function fn1(){console.log('爸爸');},true)child1.addEventListener('click',function fn1(){console.log('兒子');},true)/*當我點擊兒子的時候,我是否點擊了父親和爺爺當我點擊兒子的時候,三個函數是否調用*/// 請問fn1 fn2 fn3 的執行順序?// fn1 fn2 fn3 or fn3 fn2 fn1 </script> 復制代碼先來看結果吧:
當我們點擊id為child1的div標簽時,打印的結果是爺爺 => 爸爸 => 兒子,結果正好與事件冒泡相反。
2.事件冒泡
所謂事件冒泡就是事件像泡泡一樣從最開始生成的地方一層一層往上冒。我們只需要將addEventListener的第三個參數改為false就可以實現事件冒泡。代碼如下:
//html、css代碼同上,js代碼只是修改一下而已 var grandfather1 = document.getElementById('grandfather1'),parent1 = document.getElementById('parent1'),child1 = document.getElementById('child1');grandfather1.addEventListener('click',function fn1(){console.log('爺爺'); },false) parent1.addEventListener('click',function fn1(){console.log('爸爸'); },false) child1.addEventListener('click',function fn1(){console.log('兒子'); },false)/*當我點擊兒子的時候,我是否點擊了父親和爺爺當我點擊兒子的時候,三個函數是否調用 */ // 請問fn1 fn2 fn3 的執行順序? // fn1 fn2 fn3 or fn3 fn2 fn1 復制代碼先來看結果吧:
比如上圖中id為child1的div標簽為事件目標,點擊之后后同時也會觸發父級上的點擊事件,一層一層向上直至最外層的html或document。
注:當第三個參數為false或者為空的時候,代表在冒泡階段綁定。
四、事件代理(事件委托)
1.事件代理含義和為什么要優化?
由于事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
舉個例子,比如一個宿舍的同學同時快遞到了,一種方法就是他們都傻傻地一個個去領取,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據收件人一一分發給每個宿舍同學;
在這里,取快遞就是一個事件,每個同學指的是需要響應事件的 DOM 元素,而出去統一領取快遞的宿舍長就是代理的元素,所以真正綁定事件的是這個元素,按照收件人分發快遞的過程就是在事件執行中,需要判斷當前響應的事件應該匹配到被代理元素中的哪一個或者哪幾個。
那么利用事件冒泡或捕獲的機制,我們可以對事件綁定做一些優化。 在JS中,如果我們注冊的事件越來越多,頁面的性能就越來越差,因為:
- 函數是對象,會占用內存,內存中的對象越多,瀏覽器性能越差
- 注冊的事件一般都會指定DOM元素,事件越多,導致DOM元素訪問次數越多,會延遲頁面交互就緒時間。
- 刪除子元素的時候不用考慮刪除綁定事件
2.優點
- 減少內存消耗,提高性能
假設有一個列表,列表之中有大量的列表項,我們需要在點擊每個列表項的時候響應一個事件
// 例4 <ul id="list"><li>item 1</li><li>item 2</li><li>item 3</li>......<li>item n</li> </ul> 復制代碼如果給每個列表項一一都綁定一個函數,那對于內存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我們只需要給父容器ul綁定方法即可,這樣不管點擊的是哪一個后代元素,都會根據冒泡傳播的傳遞機制,把容器的click行為觸發,然后把對應的方法執行,根據事件源,我們可以知道點擊的是誰,從而完成不同的事。
- 動態綁定事件
在很多時候,我們需要通過用戶操作動態的增刪列表項元素,如果一開始給每個子元素綁定事件,那么在列表發生變化時,就需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件,如果用事件代理就會省去很多這樣麻煩。
2.如何實現
接下來我們來實現上例中父層元素 #list 下的 li 元素的事件委托到它的父層元素上:
<ul id="list"><li>1</li><li>2</li><li>3</li><li>4</li> </ul><script> // 給父層元素綁定事件 document.getElementById('list').addEventListener('click', function (e) {// 兼容性處理var event = e || window.event;var target = event.target || event.srcElement;// 判斷是否匹配目標元素if (target.nodeName.toLocaleLowerCase() === 'li') {console.log('the content is: ', target.innerHTML);} }); </script> 復制代碼這是常規的實現事件委托的方法,但是這種方法有BUG,當監聽的元素里存在子元素時,那么我們點擊這個子元素事件會失效,所以我們可以聯系文章上一小節說到的冒泡事件傳播機制來解決這個bug。改進的事件委托代碼:
<ul id="list"><li>1 <span>aaaaa</span></li><li>2 <span>aaaaa</span></li><li>3 <span>aaaaa</span></li><li>4</li> </ul><script>// 給父層元素綁定事件 document.getElementById('list').addEventListener('click', function (e) {// 兼容性處理var event = e || window.event;var target = event.target || event.srcElement;// 判斷是否匹配目標元素/* 從target(點擊)元素向上找currentTarget(監聽)元素,找到了想委托的元素就觸發事件,沒找到就返回null */while(target.tagName !== 'LI'){if(target.tagName === 'UL'){target = nullbreak;}target = target.parentNode}if (target) {console.log('你點擊了ul里的li')} }); 復制代碼五、Event對象常見的方法和屬性
1.event. preventDefault()
如果調用這個方法,默認事件行為將不再觸發。什么是默認事件呢?例如表單一點擊提交按鈕(submit)刷新頁面、a標簽默認頁面跳轉或是錨點定位等。
使用場景1:使用a標簽僅僅是想當做一個普通的按鈕,點擊實現一個功能,不想頁面跳轉,也不想錨點定位。
方法一
<a href="javascript:;">鏈接</a> 復制代碼方法二
使用JS方法來阻止,給其click事件綁定方法,當我們點擊A標簽的時候,先觸發click事件,其次才會執行自己的默認行為
<a id="test" href="http://www.google.com">鏈接</a> <script>test.onclick = function(e){e = e || window.event;return false;} </script> 復制代碼方法三
<a id="test" href="http://www.google.com">鏈接</a> <script>test.onclick = function(e){e = e || window.event;e.preventDefault();} </script> 復制代碼使用場景2:輸入框最多只能輸入六個字符,如何實現?
實現代碼如下:
<input type="text" id='tempInp'> <script>tempInp.onkeydown = function(ev) {ev = ev || window.event;let val = this.value.trim() //trim去除字符串首位空格(不兼容)// this.value=this.value.replace(/^ +| +$/g,'') 兼容寫法let len = val.lengthif (len >= 6) {this.value = val.substr(0, 6);//阻止默認行為去除特殊按鍵(DELETE\BACK-SPACE\方向鍵...)let code = ev.which || ev.keyCode;if (!/^(46|8|37|38|39|40)$/.test(code)) {ev.preventDefault()}}} </script> 復制代碼2.event.stopPropagation() & event.stopImmediatePropagation()
event.stopPropagation() 方法阻止事件冒泡到父元素,阻止任何父事件處理程序被執行。demo代碼如下:
// 在事件冒泡demo代碼的基礎上修改一下 child1.addEventListener('click',function fn1(e){console.log('兒子');e.stopPropagation() },false) 復制代碼stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件類型的其它監聽器被觸發。而 stopPropagation 只能實現前者的效果。我們來看個例子:
<button id="btn">點我試試</button> <script> const btn = document.querySelector('#btn'); btn.addEventListener('click', event => {console.log('btn click 1');event.stopImmediatePropagation(); }); btn.addEventListener('click', event => {console.log('btn click 2'); }); document.body.addEventListener('click', () => {console.log('body click'); }); </script> 復制代碼根據打印出來的結果,我們發現使用 stopImmediatePropagation后,點擊按鈕時,不僅body綁定事件不會觸發,與此同時按鈕的另一個點擊事件也不觸發。
3.event.target & event.currentTarget
從上面這張圖片中我們可以看到,event.target指向引起觸發事件的元素,而event.currentTarget則是事件綁定的元素。
總結
因此不必記什么時候e.currentTarget和e.target相等,什么時候不等,理解兩者的究竟指向的是誰即可。
- e.target 指向觸發事件監聽的對象「事件的真正發出者」。
- e.currentTarget 指向添加監聽事件的對象「監聽事件者」。
六、參考文章
-
DOM 事件深入淺出(一)
-
DOM事件機制解惑
-
事件模型
-
JavaScript 事件委托詳解
-
event.target和event.currentTarget的區別
-
DOM事件機制
總結
以上是生活随笔為你收集整理的深入理解DOM事件机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习:springMVC注解
- 下一篇: hostingranking.cn·基于