DOM 事件模型
DOM 的事件操作(監聽和觸發),都定義在EventTarget接口。所有節點對象都部署了這個接口,其他一些需要事件通信的瀏覽器內置對象(比如,XMLHttpRequest、AudioNode、AudioContext)也部署了這個接口。
該接口主要提供三個實例方法。
addEventListener
:綁定事件的監聽函數removeEventListener
:移除事件的監聽函數dispatchEvent
:觸發事件
事件模型
一個事件發生后,會在子元素及父元素之間進行傳播(propagation),這種傳播分為三個階段。
(這種三階段的傳播模型,使得同一個事件會在多個節點上觸發。)
- 由外向內找監聽函數就是事件捕獲
- 在目標節點觸發事件
- 由內而外找監聽函數就是事件冒泡
通俗一點來說就是一個事件被觸發時,瀏覽器會自動從用戶操作標簽外的最上級標簽逐漸向里檢查是否有相同事件,如果有則觸發,如果沒有則繼續向下檢查知道用戶操作的標簽,這過程稱為捕獲,此時瀏覽器會繼續由用戶操作標簽繼續向是上級標簽檢查,如果有相同事件則觸發,如果沒有則繼續向上檢查直到最上級元素為止,此過程稱為冒泡。(有監聽函數就執行,并提供事件信息,沒有就跳過)
事件傳播的最上層對象是window,上例的事件傳播順序,在捕獲階段依次為window、document、html、body、父節點、目標節點,在冒泡階段依次為目標節點、父節點、body、html、document、window。
DOM事件傳播的三個階段:捕獲階段,目標階段,冒泡階段
點擊事件
代碼:
<div class="grandfather"><div class="father"><div class="son"></div>word</div>
</div>
即.grandfather>.father>.son
給三個div分別添加事件的監聽fnYe/fnBa/fnEr
提問1:點擊了誰?
點擊文字,算不算點擊兒子?
點擊文字,算不算點擊爸爸?
點擊文字,算不算點擊爺爺?
答案:都算
提問2:調用循序
點擊文字,最先調用fnYe/fnBa/fnEr中的那一個函數?
答案:都行
IE5認為先調用fnEr,網景認為先調用fnYe,最后遇到了W3C
2002年,w3c發布標準
文檔名為DOM Level 2 Events Specification
規定瀏覽器應該同時支持兩種調用順序
首先按照grandfather->father->son
然后按照son->father->grandfather
術語:
從外向內找監聽函數,叫做事件捕捉
從內向外找監聽函數,叫做事件冒泡
那豈不是fnYe/fnBa/fnEr都調用兩次,非也!
開發者可以自己決定把fnYe放在捕捉階段還是放在冒泡階段

addEventListener事件綁定API
IE5*:baba.attachEvent('onclick',fn)//冒泡
網景:baba.addEventListener('click',fn)//捕獲
W3C:baba.addEventListener('click',fn,bool)
如果bool不傳或為falsy
就讓fn走冒泡,即當瀏覽器在冒泡階段發現baba有fn監聽函數,就會調用fn,并提供時間信息。
如果bool為true
就讓fn走捕獲,即當瀏覽器在捕獲階段發現baba有fn監聽函數,就會調用fn,并且提供事件信息。

代碼演示:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>JS Bin</title>
</head>
<body>
<div class="level1 x"><div class="level2 x"><div class="level3 x"><div class="level4 x"><div class="level5 x"><div class="level6 x"><div class="level7 x"></div></div></div></div></div></div>
</div></body>
</html>
CSS:
* {box-sizing: border-box;
}
div[class^=level] {border: 1px solid;border-radius: 50%;display: inline-flex;
}
.level1 {padding: 10px;background: purple;
}
.level2 {padding: 10px;background: blue;
}
.level3 {padding: 10px;background: cyan;
}
.level4 {padding: 10px;background: green;
}
.level5 {padding: 10px;background: yellow;
}
.level6 {padding: 10px;background: orange;
}
.level7 {width: 50px;height: 50px;border: 1px solid;background: red;border-radius: 50%;
}
.x{background: transparent;//把元素的變為透明
}
Javascript代碼:
const level1 = document.querySelector('.level1')
const level2 = document.querySelector('.level2')
const level3 = document.querySelector('.level3')
const level4 = document.querySelector('.level4')
const level5 = document.querySelector('.level5')
const level6 = document.querySelector('.level6')
const level7 = document.querySelector('.level7')let n = 1level1.addEventListener('click', (e)=>{const t = e.currentTarget//e只有在點擊得一瞬間才會出現,所以要用t來記錄一下。setTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1//因為如果每個時間都為1000那么就相當于在8點同時設置很多的鬧鐘,。知識
})
level2.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
level3.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
level4.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
level5.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
level6.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
level7.addEventListener('click', (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
})
簡化:
const level1 = document.querySelector('.level1')
const level2 = document.querySelector('.level2')
const level3 = document.querySelector('.level3')
const level4 = document.querySelector('.level4')
const level5 = document.querySelector('.level5')
const level6 = document.querySelector('.level6')
const level7 = document.querySelector('.level7')let n = 1const fm = (e)=>{const t = e.currentTargetsetTimeout(()=>{ t.classList.remove('x')},n*1000)n+=1
}const fa = (e)=>{const t =e.currentTargetsetTimeout(()=>{t.classList.add('x')},n*1000)n+=1}level1.addEventListener('click',fm,true)
level1.addEventListener('click',fa)
level2.addEventListener('click',fm,true)
level2.addEventListener('click',fa)
level3.addEventListener('click',fm,true)
level3.addEventListener('click',fa)
level4.addEventListener('click',fm,true)
level4.addEventListener('click',fa)
level5.addEventListener('click',fm,true)
level5.addEventListener('click',fa)
level6.addEventListener('click',fm,true)
level6.addEventListener('click',fa)
level7.addEventListener('click',fm,true)
level7.addEventListener('click',fa)
知識復習:
classList:
定義和用法
classList 屬性返回元素的類名,作為 DOMTokenList 對象。
該屬性用于在元素中添加,移除及切換 CSS 類。
classList 屬性是只讀的,但你可以使用 add() 和 remove() 方法修改它。
HTML DOM classList 屬性?www.runoob.comcurrentTarget 事件屬性
定義和用法
currentTarget 事件屬性返回其監聽器觸發事件的節點,即當前處理該事件的元素、文檔或窗口。
在捕獲和起泡階段,該屬性是非常有用的,因為在這兩個節點,它不同于 target 屬性。
總結:
兩個疑問:
兒子被點擊,算不算點擊老子?
那么先調用老子得函數還是先調用兒子的函數?
捕獲冒泡
捕獲說先調用爸爸的監聽函數
冒泡說先調用兒子的監聽函數
W3C時間模型
先捕獲(先爸爸=>兒子)再冒泡(再兒子=>爸爸)
注意e對象被傳給所有的監聽函數
事件結束后,e對象就不存在了
target v.s. currentTarget的區別
區別:
e.target - 用戶操作的元素
e.currentTarget-程序員監聽的元素
this是e.currentTarget,我個人不推薦使用它
舉例:
div>span{文字},用戶點擊文字
e.target就是span
e.currentTarget就是div
一個特例
背景:
只有一個div被監聽(不考慮父子同時被監聽)
fn分別再捕獲階段和冒泡階段監聽click事件
用戶點擊的元素就是開發者監聽的
代碼:
div.addEventListenter('click',f1)
div.addEventListenter('click',f2,true)
請問,f1先執行還是f2先執行?
如果把兩個調換位置?
總結:誰先監聽誰先執行。
level7.addEventListener('click',()=>{console.log(2)
},true)//捕獲
level7.addEventListener('click',()=>{console.log(1)
})//冒泡
e.stopPropagation():取消冒泡
e.stopPropagation()可打斷冒泡,瀏覽器不再向上走
一般用于封裝某些獨立組件
注意:捕獲不可以取消但是冒泡可以
不可以取消冒泡
有些事件不可以取消冒泡
可以查閱MDN英文版冒泡
比如scroll:

Bubbles:冒泡
Cancelable:是否取消冒泡
如何禁用滾動
取消特定元素的wheel和touchstart的默認動作
JS Bin?js.jirengu.com
瀏覽器自帶事件
來自MDN:
事件參考?developer.mozilla.org
自定義事件:代碼
JS Bin?js.jirengu.com

事件委托:
我委托一個元素幫我監聽我本該監聽的東西,比如onclick
場景1:
要給100個按鈕添加點擊事件,咋辦?
答:監聽這個100個按鈕的祖先,等冒泡的時候判斷target是不是這100個按鈕中的一個
代碼:
JS Bin?js.jirengu.com
場景2:
你要監聽目前不存在的元素的點擊事件?
答:監聽祖先,等點擊的時候看看是不是監聽的元素即可。
優點:省監聽數(內存),可以動態監聽元素
代碼:
JS Bin?js.jirengu.com
封裝一個事件委托
只要實行一個函數就可以實現事件委托
要求:
寫出這樣一個函數on('click','#testDiv','li',fn)
當用戶點擊#testDiv里面的li元素時,調用fn函數
要求用到事件委托
答案1:判斷target是否匹配'li'
答案2:target/target的爸爸/target的爺爺
代碼:
JS Bin?js.jirengu.com
錯的但是面試可以用:
答:給一個元素加一個監聽,看當前的target是否滿足監聽函數(函數2)中函數2的條件如果滿足調用,不滿足放過。但是是錯的!
代碼:
setTimeout(()=>{const button = document.createElement('button')const span = document.createElement('span')span.textContent='click 1'button.appendChild(span)div1.appendChild(button)
},1000)on('click','#div1','button',()=>{//'#div'是選擇器不是元素console.log('button 被點擊啦')
})
function on(eventType,element,selector,fn){if(!(element instanceof Element)){element = document.querySelector(element)}element.addEventListener(eventType,(e)=>{const t= e.target//被點擊的元素是span不是button啦if(t.matches(selector)){//matches用來判斷一個元素是否匹配一個選擇器,selector是不是一個選擇器
span不匹配buttonfn(e)}
})
}