前言
本文主要介紹:
- DOM事件級別
- DOM事件流
- DOM事件模型
- 事件代理
- Event對象常見的方法和屬性
一豪娜、DOM事件級別
針對不同級別的DOM,我們的DOM事件處理方式也是不一樣的哟楷。
DOM級別一共可以分為4個級別:DOM0級「通常把DOM1規(guī)范形成之前的叫做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);
// 打印的結(jié)果如下:這個函數(shù)里面包括著fn,點擊之后并沒有彈出1
/*
? onclick(event) {
fn
}
*/
</script>
// fn 加括號账千,這里就不重復(fù)寫上面代碼,只需要修改一下上面即可
<button type="button" onclick="fn()" id="btn">點我試試</button>
<script>
// 打印的結(jié)果如下:點擊之后可以彈出1
/*
? onclick(event) {
fn()
}
*/
</script>
上面的代碼我們通過直接在HTML代碼當(dāng)中定義了一個onclick的屬性觸發(fā)fn方法鞭衩,這樣的事件處理程序最大的缺點就是HTML與JS強耦合论衍,當(dāng)我們一旦需要修改函數(shù)名就得修改兩個地方坯台。當(dāng)然其優(yōu)點就是不需要操作DOM來完成事件的綁定瘫寝。
DOM0事件綁定,給元素的事件行為綁定方法咪啡,這些方法都是在當(dāng)前元素事件行為的冒泡階段(或者目標(biāo)階段)執(zhí)行的撤摸。
那我們?nèi)绾螌崿F(xiàn)HTML與JS低耦合准夷?這樣就有DOM0級處理事件的出現(xiàn)解決這個問題衫嵌。DOM0級事件就是將一個函數(shù)賦值給一個事件處理屬性渐扮,比如:
<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的按鈕耻讽,并將一個函數(shù)賦值給了一個事件處理屬性onclick针肥,這樣的方法便是DOM0級處理事件的體現(xiàn)慰枕。我們可以通過給事件處理屬性賦值null來解綁事件。DOM 0級的事件處理的步驟:先找到DOM節(jié)點博肋,然后把處理函數(shù)賦值給該節(jié)點對象的事件屬性匪凡。
DOM0級事件處理程序的缺點在于一個處理程序「事件」無法同時綁定多個處理函數(shù)病游,比如我還想在按鈕點擊事件上加上另外一個函數(shù)衬衬。
var btn = document.getElementById('btn');
btn.onclick = function() {
alert('Hello World');
}
btn.onclick = function() {
alert('沒想到吧佣耐,我執(zhí)行了兼砖,哈哈哈');
}
2.DOM2級事件
DOM2級事件在DOM0級事件的基礎(chǔ)上彌補了一個處理程序無法同時綁定多個處理函數(shù)的缺點讽挟,允許給一個處理程序添加多個處理函數(shù)耽梅。也就是說眼姐,使用DOM2事件可以隨意添加多個處理函數(shù)佩番,移除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個參數(shù)著洼,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數(shù)年碘、是否在捕獲時執(zhí)行事件處理函數(shù)(關(guān)于事件冒泡和事件捕獲下面會介紹)
*/
注:
IE8級以下版本不支持addEventListener和removeEventListener屿衅,需要用attachEvent和detachEvent來實現(xiàn):
// IE8級以下版本只支持冒泡型事件,不支持事件捕獲所以沒有第三個參數(shù)
// 方法中包含2個參數(shù)涤久,分別是綁定的事件處理屬性名稱(不包含on)、事件處理函數(shù)
btn.attachEvent('onclick', fn); // 綁定事件
btn.detachEvent('onclick', fn); // 解綁事件
3.DOM3級事件
DOM3級事件在DOM2級事件的基礎(chǔ)上添加了更多的事件類型考抄,全部類型如下:
- UI事件川梅,當(dāng)用戶與頁面上的元素交互時觸發(fā)贫途,如:load待侵、scroll
- 焦點事件秧倾,當(dāng)元素獲得或失去焦點時觸發(fā)那先,如:blur胃榕、focus
- 鼠標(biāo)事件勋又,當(dāng)用戶通過鼠標(biāo)在頁面執(zhí)行操作時觸發(fā)如:dbclick楔壤、mouseup
- 滾輪事件,當(dāng)使用鼠標(biāo)滾輪或類似設(shè)備時觸發(fā)递瑰,如:mousewheel
- 文本事件抖部,當(dāng)在文檔中輸入文本時觸發(fā)慎颗,如:textInput
- 鍵盤事件,當(dāng)用戶通過鍵盤在頁面上執(zhí)行操作時觸發(fā)傲宜,如:keydown函卒、keypress
- 合成事件撇眯,當(dāng)為IME(輸入法編輯器)輸入字符時觸發(fā),如:compositionstart
- 變動事件彤钟,當(dāng)?shù)讓覦OM結(jié)構(gòu)發(fā)生變化時觸發(fā)逸雹,如:DOMsubtreeModified
同時DOM3級事件也允許使用者自定義一些事件梆砸。
DOM事件級別的發(fā)展使得事件處理更加完整豐富帖世,而下一個問題就是之前提到的DOM事件模型÷腹「事件冒泡和事件捕獲」
二盈魁、DOM事件流
為什么是有事件流杨耙?
假如在一個button上注冊了一個click事件珊膜,又在其它父元素div上注冊了一個click事件辅搬,那么當(dāng)我們點擊button堪遂,是先觸發(fā)父元素上的事件,還是button上的事件呢币旧,這就需要一種約定去規(guī)范事件的執(zhí)行順序吹菱,就是事件執(zhí)行的流程。
瀏覽器在發(fā)展的過程中出現(xiàn)了兩種不同的規(guī)范
- IE9以下的IE瀏覽器使用的是事件冒泡俯抖,先從具體的接收元素芬萍,然后逐步向上傳播到不具體的元素柬祠。
- Netscapte采用的是事件捕獲,先由不具體的元素接收事件嗜愈,最具體的節(jié)點最后才接收到事件芝硬。
- 而W3C制定的Web標(biāo)準(zhǔn)中轧房,是同時采用了兩種方案迟赃,事件捕獲和事件冒泡都可以厂镇。
三捺信、DOM事件模型
DOM事件模型分為捕獲和冒泡。一個事件發(fā)生后喇辽,會在子元素和父元素之間傳播(propagation)菩咨。這種傳播分成三個階段抽米。
(1)捕獲階段:事件從window對象自上而下向目標(biāo)節(jié)點傳播的階段云茸;
(2)目標(biāo)階段:真正的目標(biāo)節(jié)點正在處理事件的階段标捺;
(3)冒泡階段:事件從目標(biāo)節(jié)點自下而上向window對象傳播的階段宜岛。
上文中講到了addEventListener的第三個參數(shù)為指定事件是否在捕獲或冒泡階段執(zhí)行,設(shè)置為true表示事件在捕獲階段執(zhí)行辟汰,而設(shè)置為false表示事件在冒泡階段執(zhí)行帖汞。那么什么是事件冒泡和事件捕獲呢翩蘸?可以用下圖來解釋:
1.事件捕獲
捕獲是從上到下催首,事件先從window對象郎任,然后再到document(對象)舶治,然后是html標(biāo)簽(通過document.documentElement獲取html標(biāo)簽)霉猛,然后是body標(biāo)簽(通過document.body獲取body標(biāo)簽)韩脏,然后按照普通的html結(jié)構(gòu)一層一層往下傳杭朱,最后到達目標(biāo)元素弧械。我們只需要將addEventListener的第三個參數(shù)改為true就可以實現(xiàn)事件捕獲刃唐。代碼如下:
<!-- 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)
/*
當(dāng)我點擊兒子的時候,我是否點擊了父親和爺爺
當(dāng)我點擊兒子的時候浊猾,三個函數(shù)是否調(diào)用
*/
// 請問fn1 fn2 fn3 的執(zhí)行順序衔彻?
// fn1 fn2 fn3 or fn3 fn2 fn1
</script>
先來看結(jié)果吧:
當(dāng)我們點擊id為child1的div標(biāo)簽時艰额,打印的結(jié)果是爺爺 => 爸爸 => 兒子,結(jié)果正好與事件冒泡相反祖搓。
2.事件冒泡
所謂事件冒泡就是事件像泡泡一樣從最開始生成的地方一層一層往上冒泪喊。我們只需要將addEventListener的第三個參數(shù)改為false就可以實現(xiàn)事件冒泡棕硫。代碼如下:
//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)
/*
當(dāng)我點擊兒子的時候哈扮,我是否點擊了父親和爺爺
當(dāng)我點擊兒子的時候纬纪,三個函數(shù)是否調(diào)用
*/
// 請問fn1 fn2 fn3 的執(zhí)行順序?
// fn1 fn2 fn3 or fn3 fn2 fn1
先來看結(jié)果吧:
比如上圖中id為child1的div標(biāo)簽為事件目標(biāo)滑肉,點擊之后后同時也會觸發(fā)父級上的點擊事件包各,一層一層向上直至最外層的html或document。
注:當(dāng)?shù)谌齻€參數(shù)為false
或者為空的時候靶庙,代表在冒泡階段綁定。
四卵皂、事件代理(事件委托)
1.事件代理含義和為什么要優(yōu)化捅膘?
由于事件會在冒泡階段向上傳播到父節(jié)點愧沟,因此可以把子節(jié)點的監(jiān)聽函數(shù)定義在父節(jié)點上,由父節(jié)點的監(jiān)聽函數(shù)統(tǒng)一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
舉個例子,比如一個宿舍的同學(xué)同時快遞到了绘趋,一種方法就是他們都傻傻地一個個去領(lǐng)取垦江,還有一種方法就是把這件事情委托給宿舍長,讓一個人出去拿好所有快遞,然后再根據(jù)收件人一一分發(fā)給每個宿舍同學(xué)底哗;
在這里,取快遞就是一個事件坠韩,每個同學(xué)指的是需要響應(yīng)事件的 DOM 元素俭尖,而出去統(tǒng)一領(lǐng)取快遞的宿舍長就是代理的元素已亥,所以真正綁定事件的是這個元素的妖,按照收件人分發(fā)快遞的過程就是在事件執(zhí)行中星虹,需要判斷當(dāng)前響應(yīng)的事件應(yīng)該匹配到被代理元素中的哪一個或者哪幾個卸亮。
那么利用事件冒泡或捕獲的機制溶诞,我們可以對事件綁定做一些優(yōu)化枉圃。
在JS中肄扎,如果我們注冊的事件越來越多衡载,頁面的性能就越來越差,因為:
- 函數(shù)是對象,會占用內(nèi)存遍坟,內(nèi)存中的對象越多隔节,瀏覽器性能越差
- 注冊的事件一般都會指定DOM元素刽虹,事件越多,導(dǎo)致DOM元素訪問次數(shù)越多肾筐,會延遲頁面交互就緒時間奋渔。
- 刪除子元素的時候不用考慮刪除綁定事件
2.優(yōu)點
- 減少內(nèi)存消耗捻爷,提高性能
假設(shè)有一個列表骂远,列表之中有大量的列表項瘸恼,我們需要在點擊每個列表項的時候響應(yīng)一個事件
// 例4
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果給每個列表項一一都綁定一個函數(shù)拦键,那對于內(nèi)存消耗是非常大的莲镣,效率上需要消耗很多性能。借助事件代理店归,我們只需要給父容器ul綁定方法即可都哭,這樣不管點擊的是哪一個后代元素怒炸,都會根據(jù)冒泡傳播的傳遞機制教寂,把容器的click行為觸發(fā),然后把對應(yīng)的方法執(zhí)行,根據(jù)事件源媳拴,我們可以知道點擊的是誰,從而完成不同的事髓削。
- 動態(tài)綁定事件
在很多時候立膛,我們需要通過用戶操作動態(tài)的增刪列表項元素,如果一開始給每個子元素綁定事件梯码,那么在列表發(fā)生變化時宝泵,就需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件轩娶,如果用事件代理就會省去很多這樣麻煩儿奶。
2.如何實現(xiàn)
接下來我們來實現(xiàn)上例中父層元素 #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;
// 判斷是否匹配目標(biāo)元素
if (target.nodeName.toLocaleLowerCase() === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
</script>
這是常規(guī)的實現(xiàn)事件委托的方法,但是這種方法有BUG鳄抒,當(dāng)監(jiān)聽的元素里存在子元素時闯捎,那么我們點擊這個子元素事件會失效椰弊,所以我們可以聯(lián)系文章上一小節(jié)說到的冒泡事件傳播機制來解決這個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;
// 判斷是否匹配目標(biāo)元素
/* 從target(點擊)元素向上找currentTarget(監(jiān)聽)元素瓤鼻,
找到了想委托的元素就觸發(fā)事件秉版,沒找到就返回null */
while(target.tagName !== 'LI'){
if(target.tagName === 'UL'){
target = null
break;
}
target = target.parentNode
}
if (target) {
console.log('你點擊了ul里的li')
}
});
五、Event對象常見的方法和屬性
1.event. preventDefault()
如果調(diào)用這個方法娱仔,默認事件行為將不再觸發(fā)沐飘。什么是默認事件呢游桩?例如表單一點擊提交按鈕(submit)刷新頁面牲迫、a標(biāo)簽?zāi)J頁面跳轉(zhuǎn)或是錨點定位等。
使用場景1:使用a標(biāo)簽僅僅是想當(dāng)做一個普通的按鈕借卧,點擊實現(xiàn)一個功能盹憎,不想頁面跳轉(zhuǎn),也不想錨點定位铐刘。
方法一
<a href="javascript:;">鏈接</a>
方法二
使用JS方法來阻止陪每,給其click事件綁定方法,當(dāng)我們點擊A標(biāo)簽的時候镰吵,先觸發(fā)click事件檩禾,其次才會執(zhí)行自己的默認行為
<a id="test" >鏈接</a>
<script>
test.onclick = function(e){
e = e || window.event;
return false;
}
</script>
方法三
<a id="test" >鏈接</a>
<script>
test.onclick = function(e){
e = e || window.event;
e.preventDefault();
}
</script>
使用場景2:輸入框最多只能輸入六個字符,如何實現(xiàn)疤祭?
實現(xiàn)代碼如下:
<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.length
if (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() 方法阻止事件冒泡到父元素盼产,阻止任何父事件處理程序被執(zhí)行。demo代碼如下:
// 在事件冒泡demo代碼的基礎(chǔ)上修改一下
child1.addEventListener('click',function fn1(e){
console.log('兒子');
e.stopPropagation()
},false)
stopImmediatePropagation 既能阻止事件向父元素冒泡勺馆,也能阻止元素同事件類型的其它監(jiān)聽器被觸發(fā)戏售。而 stopPropagation 只能實現(xiàn)前者的效果。我們來看個例子:
<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>
根據(jù)打印出來的結(jié)果草穆,我們發(fā)現(xiàn)使用 stopImmediatePropagation后灌灾,點擊按鈕時,不僅body綁定事件不會觸發(fā)悲柱,與此同時按鈕的另一個點擊事件也不觸發(fā)锋喜。
3.event.target & event.currentTarget
從上面這張圖片中我們可以看到,event.target
指向引起觸發(fā)事件的元素豌鸡,而event.currentTarget
則是事件綁定的元素嘿般。
總結(jié)
因此不必記什么時候e.currentTarget
和e.target
相等,什么時候不等直颅,理解兩者的究竟指向的是誰即可博个。
-
e.target
指向觸發(fā)事件監(jiān)聽的對象「事件的真正發(fā)出者」。 -
e.currentTarget
指向添加監(jiān)聽事件的對象「監(jiān)聽事件者」功偿。