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