1.事件流
在瀏覽器中,JavaScript和HTML之間的交互是通過事件去實現(xiàn)的,常用的事件有代表鼠標單擊的click事件、代表加載的load事件、代表鼠標指針懸浮的mouseover事件。在事件發(fā)生時,會相對應(yīng)地觸發(fā)綁定在元素上的事件處理程序,以處理對應(yīng)的操作。
通常一個頁面會綁定很多的事件,那么具體的事件觸發(fā)順序是什么樣的呢?
這就會涉及事件流的概念,事件流描述的是從頁面中接收事件的順序。事件發(fā)生后會在目標節(jié)點和根節(jié)點之間按照特定的順序傳播,路徑經(jīng)過的節(jié)點都會接收到事件。我們通過下面的場景來直觀地想象一下事件的流轉(zhuǎn)順序。
頁面上有一個div,分別在body、div、p、span上綁定了click事件。假如我在span上執(zhí)行了單擊的操作,那么將會產(chǎn)生什么樣的事件流呢?
<body>
<div>
<p>
<span>
文本一
</span>
</p>
</div>
</body>
第一種事件傳遞順序是先觸發(fā)最外層的body元素,然后向內(nèi)傳播,依次觸發(fā)div、p與span元素。
第二種事件傳遞順序先觸發(fā)由最內(nèi)層的span元素,然后向外傳播,依次觸發(fā)p、div與body元素。
第一種事件傳遞順序?qū)?yīng)的是捕獲型事件流,第二種事件傳遞順序?qū)?yīng)的是冒泡型事件流。
一個完整的事件流實際包含了3個階段:事件捕獲階段>事件目標階段>事件冒泡階段。上述兩種類型的事件流實際對應(yīng)其中的事件捕獲階段與事件冒泡階段。

- 事件捕獲階段
事件捕獲階段的主要表現(xiàn)是不具體的節(jié)點先接收事件,然后逐級向下傳播,最具體的節(jié)點最后接收到事件。根據(jù)圖中的指示就是Window > Document > html > body > div > p > span。 - 事件目標階段
事件目標階段表示事件剛好傳播到用戶產(chǎn)生行為的元素上,可能是事件捕獲的最后一個階段,也可能是事件冒泡的第一個階段。 - 事件冒泡階段
事件冒泡階段的主要表現(xiàn)是最具體的元素先接收事件,然后逐級向上傳播,不具體的節(jié)點最后接收事件,根據(jù)圖中的指示就是span > p > div > body > html > Document >Window。
let spanDom = document.querySelector('span');
spanDom .addEventListener("click", function (e) {
console.info(`span trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
}, false)
let p1Dom = document.querySelector('p');
p1Dom.addEventListener('click', function (e) {
console.info(`p trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
let divDom = document.querySelector('div');
divDom.addEventListener('click', function (e) {
console.info(`div trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
let bodyDom = document.querySelector('body');
bodyDom.addEventListener('click', function (e) {
console.info(`body trigger target:${e.target.tagName.toLowerCase()} currentTarget:${e.currentTarget.tagName.toLowerCase()} `);
})
當點擊span標簽時候,將輸出

使用addEventListener()函數(shù)綁定的事件在默認情況下,即第三個參數(shù)默認為false時,按照冒泡型事件流處理。第三個參數(shù)為true時,按捕獲型事件流處理,再次點擊span時候會輸出如下:

如果有元素綁定了捕獲類型事件,則會優(yōu)先于冒泡類型事件而先執(zhí)行。
2.事件處理程序
簡單理解事件處理程序,就是響應(yīng)某個事件的函數(shù),例如onclick()函數(shù)、onload()函數(shù)就是響應(yīng)單擊、加載事件的函數(shù),對應(yīng)的是一段JavaScript的函數(shù)代碼。
根據(jù)W3C DOM標準,事件處理程序分為DOM0、DOM2、DOM3這3種級別的事件處理程序。由于在DOM1中并沒有定義事件的相關(guān)內(nèi)容,因此沒有所謂的DOM1級事件處理程序。
- DOM0級事件處理程序
DOM0級事件處理程序是將一個函數(shù)賦值給一個事件處理屬性,有兩種表現(xiàn)形式。第一種是先通過JavaScript代碼獲取DOM元素,再將函數(shù)賦值給對應(yīng)的事件屬性。
var btn = document.getElementById("btn");
btn.onclick = function(){}
第二種是直接在html中設(shè)置對應(yīng)事件屬性的值,值有兩種表現(xiàn)形式,一種是執(zhí)行的函數(shù)體,另一種是函數(shù)名,然后在script標簽中定義該函數(shù)。
<button onclick="alert('hi');">單擊</button>
<button onclick="clickFn()">單擊</button>
<script>
function clickFn() {
alert('hi');
}
</script>
以上兩種DOM0級事件處理程序同時存在時,第一種在JavaScript中定義的事件處理程序會覆蓋掉后面在html標簽中定義的事件處理程序。
DOM0級事件處理程序只支持事件冒泡階段,一個事件處理程序只能綁定一個函數(shù)。
- DOM2級事件處理程序
在DOM2級事件處理程序中,當事件發(fā)生在節(jié)點時,目標元素的事件處理函數(shù)就會被觸發(fā),而且目標元素的每個祖先節(jié)點也會按照事件流順序觸發(fā)對應(yīng)的事件處理程序。DOM2級事件處理方式規(guī)定了添加事件處理程序和刪除事件處理程序的方法。
在IE10及以下版本中,只支持事件冒泡階段
element.attachEvent("on"+ eventName, handler); //添加事件處理程序
element.detachEvent("on"+ eventName, handler); //刪除事件處理程序
在IE11及其他非IE瀏覽器中,同時支持事件捕獲和事件冒泡兩個階段,可以通過addEventListener()函數(shù)添加事件處理程序,通過removeEventListener()函數(shù)刪除事件處理程序。
addEventListener(eventName, handler, useCapture); //添加事件處理程序
removeEventListener(eventName, handler, useCapture); //刪除事件處理程序
//其中的useCapture參數(shù)表示是否支持事件捕獲,true表示支持事件捕獲,false表示支持事件冒泡,默認狀態(tài)為false。
- DOM3級事件處理程序
DOM3級事件處理程序是在DOM2級事件的基礎(chǔ)上重新定義了事件,也添加了一些新的事件。最重要的區(qū)別在于DOM3級事件處理程序允許自定義事件,自定義事件由createEvent("CustomEvent")函數(shù)創(chuàng)建,返回的對象有一個initCustomEvent()函數(shù),通過傳遞對應(yīng)的參數(shù)可以自定義事件。
函數(shù)可以接收以下4個參數(shù)。
· type:字符串、觸發(fā)的事件類型、自定義,例如“keyDown”“selectedChange”。
· bubble(布爾值):表示事件是否可以冒泡。
· cancelable(布爾值):表示事件是否可以取消。
· detail(對象):任意值,保存在event對象的detail屬性中。
創(chuàng)建完成的自定義事件,可以通過dispatchEvent()函數(shù)去手動觸發(fā),觸發(fā)自定義事件的元素需要和綁定自定義事件的元素為同一個元素。
<body>
<div>
<p>
<span>
自定義事件
</span>
</p>
</div>
</body>
<script>
var customEvent;
(function () {
if (document.implementation.hasFeature('CusomEvents', '3.0')) {
let detailData = {name: 'climber.lee'}
customEvent = document.createEvent('CustomEvent');
customEvent.initCustomEvent('myEvent', true, false, detailData)
let p = document.querySelector('p');
p.addEventListener('myEvent', function (e) {
console.info(`p監(jiān)聽到自定義事件執(zhí)行,參數(shù)為:`,e.detail)
});
let span = document.querySelector('span');
span.addEventListener('click', function () {
p.dispatchEvent(customEvent);
})
}
})();
</script>
點擊span后輸出

3.事件(Event)對象
事件在瀏覽器中是以Event對象的形式存在的,每觸發(fā)一個事件,就會產(chǎn)生一個Event對象。該對象包含所有與事件相關(guān)的信息,包括事件的元素、事件的類型及其他與特定事件相關(guān)的信息。
在Event對象中有兩個屬性總是會引起大家的困擾,那就是target屬性和currentTarget屬性。兩者都可以表示事件的目標元素,但是在事件流中兩者卻有不同的意義。
· target屬性在事件目標階段,理解為真實操作的目標元素。
· currentTarget屬性在事件捕獲、事件目標、事件冒泡這3個階段,理解為當前事件流所處的某個階段對應(yīng)的目標元素。
可以參考第一部分事件流的實例來理解target和currentTarget的區(qū)別
有時我們并不想要事件進行冒泡,可通過stopPropagation和stopImmediatePropagation兩個方法阻止冒泡
· stopPropagation()函數(shù)僅會阻止事件冒泡,其他事件處理程序仍然可以調(diào)用。
· stopImmediatePropagation()函數(shù)不僅會阻止冒泡,也會阻止其他事件處理程序的調(diào)用。
在眾多的HTML標簽中,有一些標簽是具有默認行為的
· a標簽,在單擊后默認行為會跳轉(zhuǎn)至href屬性指定的鏈接中。
那么該如何編寫代碼來阻止元素的默認行為呢?
很簡單,就是通過event.preventDefault()函數(shù)去實現(xiàn)。
4.事件委托
過多事件處理程序”的解決方案是使用事件委托。事件委托利用事件冒泡,可以只使用一個事件處理程序來管理一種類型的事件。例如,click事件冒泡到document。這意味著可以為整個頁面指定一個onclick事件處理程序,而不用為每個可點擊元素分別指定事件處理程序。舉個栗子
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
這里的HTML包含3個列表項,在被點擊時應(yīng)該執(zhí)行某個操作。對此,通常的做法是像這樣指定3個事件處理程序
let item1 = document.getElementById("goSomewhere");
let item2 = document.getElementById("doSomething");
let item3 = document.getElementById("sayHi");
item1.addEventListener("click", (event) => {
location.href = "http:// www.wrox.com";
});
item2.addEventListener("click", (event) => {
document.title = "I changed the document's title";
});
item3.addEventListener("click", (event) => {
console.log("hi");
});
如果對頁面中所有需要使用onclick事件處理程序的元素都如法炮制,結(jié)果就會出現(xiàn)大片雷同的只為指定事件處理程序的代碼。使用事件委托,只要給所有元素共同的祖先節(jié)點添加一個事件處理程序,就可以解決問題。
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
}
});
事件委托具有如下優(yōu)點
- document對象隨時可用,任何時候都可以給它添加事件處理程序(不用等待DOMContentLoaded或load事件)。這意味著只要頁面渲染出可點擊的元素,就可以無延遲地起作用。
- 節(jié)省花在設(shè)置頁面事件處理程序上的時間。只指定一個事件處理程序既可以節(jié)省DOM引用,也可以節(jié)省時間。
- 減少整個頁面所需的內(nèi)存,提升整體性能。