監(jiān)聽(tīng)函數(shù)
瀏覽器的事件模型,就是通過(guò)監(jiān)聽(tīng)函數(shù)(listener)對(duì)事件做出反應(yīng)。事件發(fā)生后,瀏覽器監(jiān)聽(tīng)到了這個(gè)事件,就會(huì)執(zhí)行對(duì)應(yīng)的監(jiān)聽(tīng)函數(shù)。這是事件驅(qū)動(dòng)編程模式(event-driven)的主要編程方式。
JavaScript 有三種方法,可以為事件綁定監(jiān)聽(tīng)函數(shù)。
HTML 的 on- 屬性
HTML 語(yǔ)言允許在元素的屬性中,直接定義某些事件的監(jiān)聽(tīng)代碼。
<body onload="doSomething()">
<div onclick="console.log('觸發(fā)事件')">
上面代碼為body
節(jié)點(diǎn)的load
事件、div
節(jié)點(diǎn)的click
事件,指定了監(jiān)聽(tīng)代碼。一旦事件發(fā)生,就會(huì)執(zhí)行這段代碼。
元素的事件監(jiān)聽(tīng)屬性,都是on
加上事件名,比如onload
就是on + load
,表示load
事件的監(jiān)聽(tīng)代碼。
注意,這些屬性的值是將會(huì)執(zhí)行的代碼,而不是一個(gè)函數(shù)。
<!-- 正確 -->
<body onload="doSomething()">
<!-- 錯(cuò)誤 -->
<body onload="doSomething">
一旦指定的事件發(fā)生,on-
屬性的值是原樣傳入 JavaScript 引擎執(zhí)行。因此如果要執(zhí)行函數(shù),不要忘記加上一對(duì)圓括號(hào)。
使用這個(gè)方法指定的監(jiān)聽(tīng)代碼,只會(huì)在冒泡階段觸發(fā)。
<div onclick="console.log(2)">
<button onclick="console.log(1)">點(diǎn)擊</button>
</div>
上面代碼中,<button>
是<div>
的子元素。<button>
的click
事件,也會(huì)觸發(fā)<div>
的click
事件。由于on-
屬性的監(jiān)聽(tīng)代碼,只在冒泡階段觸發(fā),所以點(diǎn)擊結(jié)果是先輸出1
,再輸出2
,即事件從子元素開(kāi)始冒泡到父元素。
直接設(shè)置on-
屬性,與通過(guò)元素節(jié)點(diǎn)的setAttribute
方法設(shè)置on-
屬性,效果是一樣的。
el.setAttribute('onclick', 'doSomething()');
// 等同于
// <Element onclick="doSomething()">
元素節(jié)點(diǎn)的事件屬性
元素節(jié)點(diǎn)對(duì)象的事件屬性,同樣可以指定監(jiān)聽(tīng)函數(shù)。
window.onload = doSomething;
div.onclick = function (event) {
console.log('觸發(fā)事件');
};
使用這個(gè)方法指定的監(jiān)聽(tīng)函數(shù),也是只會(huì)在冒泡階段觸發(fā)。
注意,這種方法與 HTML 的on-
屬性的差異是,它的值是函數(shù)名(doSomething
),而不像后者,必須給出完整的監(jiān)聽(tīng)代碼(doSomething()
)。
EventTarget.addEventListener()
所有 DOM 節(jié)點(diǎn)實(shí)例都有addEventListener
方法,用來(lái)為該節(jié)點(diǎn)定義事件的監(jiān)聽(tīng)函數(shù)。
window.addEventListener('load', doSomething, false);
addEventListener
方法的詳細(xì)介紹,參見(jiàn)EventTarget
章節(jié)。
小結(jié) #
上面三種方法,第一種“HTML 的 on- 屬性”,違反了 HTML 與 JavaScript 代碼相分離的原則,將兩者寫(xiě)在一起,不利于代碼分工,因此不推薦使用。
第二種“元素節(jié)點(diǎn)的事件屬性”的缺點(diǎn)在于,同一個(gè)事件只能定義一個(gè)監(jiān)聽(tīng)函數(shù),也就是說(shuō),如果定義兩次onclick
屬性,后一次定義會(huì)覆蓋前一次。因此,也不推薦使用。
第三種EventTarget.addEventListener
是推薦的指定監(jiān)聽(tīng)函數(shù)的方法。它有如下優(yōu)點(diǎn):
- 同一個(gè)事件可以添加多個(gè)監(jiān)聽(tīng)函數(shù)。
- 能夠指定在哪個(gè)階段(捕獲階段還是冒泡階段)觸發(fā)監(jiān)聽(tīng)函數(shù)。
- 除了 DOM 節(jié)點(diǎn),其他對(duì)象(比如
window
、XMLHttpRequest
等)也有這個(gè)接口,它等于是整個(gè) JavaScript 統(tǒng)一的監(jiān)聽(tīng)函數(shù)接口。
this 的指向 #
監(jiān)聽(tīng)函數(shù)內(nèi)部的this
指向觸發(fā)事件的那個(gè)元素節(jié)點(diǎn)。
<button id="btn" onclick="console.log(this.id)">點(diǎn)擊</button>
執(zhí)行上面代碼,點(diǎn)擊后會(huì)輸出btn
。
其他兩種監(jiān)聽(tīng)函數(shù)的寫(xiě)法,this
的指向也是如此。
// HTML 代碼如下
// <button id="btn">點(diǎn)擊</button>
var btn = document.getElementById('btn');
// 寫(xiě)法一
btn.onclick = function () {
console.log(this.id);
};
// 寫(xiě)法二
btn.addEventListener(
'click',
function (e) {
console.log(this.id);
},
false
);
上面兩種寫(xiě)法,點(diǎn)擊按鈕以后也是輸出btn
。
事件的傳播 #
一個(gè)事件發(fā)生后,會(huì)在子元素和父元素之間傳播(propagation)。這種傳播分成三個(gè)階段。
- 第一階段:從
window
對(duì)象傳導(dǎo)到目標(biāo)節(jié)點(diǎn)(上層傳到底層),稱為“捕獲階段”(capture phase)。 - 第二階段:在目標(biāo)節(jié)點(diǎn)上觸發(fā),稱為“目標(biāo)階段”(target phase)。
- 第三階段:從目標(biāo)節(jié)點(diǎn)傳導(dǎo)回
window
對(duì)象(從底層傳回上層),稱為“冒泡階段”(bubbling phase)。
這種三階段的傳播模型,使得同一個(gè)事件會(huì)在多個(gè)節(jié)點(diǎn)上觸發(fā)。
<div>
<p>點(diǎn)擊</p>
</div>
上面代碼中,<div>
節(jié)點(diǎn)之中有一個(gè)<p>
節(jié)點(diǎn)。
如果對(duì)這兩個(gè)節(jié)點(diǎn),都設(shè)置click
事件的監(jiān)聽(tīng)函數(shù)(每個(gè)節(jié)點(diǎn)的捕獲階段和冒泡階段,各設(shè)置一個(gè)監(jiān)聽(tīng)函數(shù)),共計(jì)設(shè)置四個(gè)監(jiān)聽(tīng)函數(shù)。然后,對(duì)<p>
點(diǎn)擊,click
事件會(huì)觸發(fā)四次。
var phases = {
1: 'capture',
2: 'target',
3: 'bubble'
};
var div = document.querySelector('div');
var p = document.querySelector('p');
div.addEventListener('click', callback, true);
p.addEventListener('click', callback, true);
div.addEventListener('click', callback, false);
p.addEventListener('click', callback, false);
function callback(event) {
var tag = event.currentTarget.tagName;
var phase = phases[event.eventPhase];
console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");
}
// 點(diǎn)擊以后的結(jié)果
// Tag: 'DIV'. EventPhase: 'capture'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'P'. EventPhase: 'target'
// Tag: 'DIV'. EventPhase: 'bubble'
上面代碼表示,click
事件被觸發(fā)了四次:<div>
節(jié)點(diǎn)的捕獲階段和冒泡階段各1次,<p>
節(jié)點(diǎn)的目標(biāo)階段觸發(fā)了2次。
- 捕獲階段:事件從
<div>
向<p>
傳播時(shí),觸發(fā)<div>
的click
事件; - 目標(biāo)階段:事件從
<div>
到達(dá)<p>
時(shí),觸發(fā)<p>
的click
事件; - 冒泡階段:事件從
<p>
傳回<div>
時(shí),再次觸發(fā)<div>
的click
事件。
其中,<p>
節(jié)點(diǎn)有兩個(gè)監(jiān)聽(tīng)函數(shù)(addEventListener
方法第三個(gè)參數(shù)的不同,會(huì)導(dǎo)致綁定兩個(gè)監(jiān)聽(tīng)函數(shù)),因此它們都會(huì)因?yàn)?code>click事件觸發(fā)一次。所以,<p>
會(huì)在target
階段有兩次輸出。
注意,瀏覽器總是假定click
事件的目標(biāo)節(jié)點(diǎn),就是點(diǎn)擊位置嵌套最深的那個(gè)節(jié)點(diǎn)(本例是<div>
節(jié)點(diǎn)里面的<p>
節(jié)點(diǎn))。所以,<p>
節(jié)點(diǎn)的捕獲階段和冒泡階段,都會(huì)顯示為target
階段。
事件傳播的最上層對(duì)象是window
,接著依次是document
,html
(document.documentElement
)和body
(document.body
)。也就是說(shuō),上例的事件傳播順序,在捕獲階段依次為window
、document
、html
、body
、div
、p
,在冒泡階段依次為p
、div
、body
、html
、document
、window
。
事件的代理 #
由于事件會(huì)在冒泡階段向上傳播到父節(jié)點(diǎn),因此可以把子節(jié)點(diǎn)的監(jiān)聽(tīng)函數(shù)定義在父節(jié)點(diǎn)上,由父節(jié)點(diǎn)的監(jiān)聽(tīng)函數(shù)統(tǒng)一處理多個(gè)子元素的事件。這種方法叫做事件的代理(delegation)。
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
上面代碼中,click
事件的監(jiān)聽(tīng)函數(shù)定義在<ul>
節(jié)點(diǎn),但是實(shí)際上,它處理的是子節(jié)點(diǎn)<li>
的click
事件。這樣做的好處是,只要定義一個(gè)監(jiān)聽(tīng)函數(shù),就能處理多個(gè)子節(jié)點(diǎn)的事件,而不用在每個(gè)<li>
節(jié)點(diǎn)上定義監(jiān)聽(tīng)函數(shù)。而且以后再添加子節(jié)點(diǎn),監(jiān)聽(tīng)函數(shù)依然有效。
如果希望事件到某個(gè)節(jié)點(diǎn)為止,不再傳播,可以使用事件對(duì)象的stopPropagation
方法。
// 事件傳播到 p 元素后,就不再向下傳播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
上面代碼中,stopPropagation
方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。
但是,stopPropagation
方法只會(huì)阻止事件的傳播,不會(huì)阻止該事件觸發(fā)<p>
節(jié)點(diǎn)的其他click
事件的監(jiān)聽(tīng)函數(shù)。也就是說(shuō),不是徹底取消click
事件。
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 會(huì)觸發(fā)
console.log(2);
});
上面代碼中,p
元素綁定了兩個(gè)click
事件的監(jiān)聽(tīng)函數(shù)。stopPropagation
方法只能阻止這個(gè)事件的傳播,不能取消這個(gè)事件,因此,第二個(gè)監(jiān)聽(tīng)函數(shù)會(huì)觸發(fā)。輸出結(jié)果會(huì)先是1,然后是2。
如果想要徹底取消該事件,不再觸發(fā)后面所有click
的監(jiān)聽(tīng)函數(shù),可以使用stopImmediatePropagation
方法。
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不會(huì)被觸發(fā)
console.log(2);
});
上面代碼中,stopImmediatePropagation
方法可以徹底取消這個(gè)事件,使得后面綁定的所有click
監(jiān)聽(tīng)函數(shù)都不再觸發(fā)。所以,只會(huì)輸出1,不會(huì)輸出2。
更多建議: