溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

js基礎(chǔ)之事件捕獲與冒泡原理

發(fā)布時(shí)間:2020-09-19 12:06:26 來源:腳本之家 閱讀:120 作者:夕山雨 欄目:web開發(fā)

想要了解什么是事件捕獲與冒泡,需要先了解什么是事件。

什么是事件?

我們知道,在前端開發(fā)中,JavaScript負(fù)責(zé)定義網(wǎng)頁(yè)的“行為”。這里所說的“定義”,其實(shí)指的是開發(fā)者可以通過JavaScript語(yǔ)言向?yàn)g覽器描述一些規(guī)則,瀏覽器按照這些規(guī)則與用戶進(jìn)行交互。比如開發(fā)者希望當(dāng)用戶點(diǎn)擊頁(yè)面上某個(gè)按鈕的時(shí)候,就彈出一個(gè)窗口,顯示特定的內(nèi)容。而當(dāng)用戶真正點(diǎn)擊這個(gè)按鈕的時(shí)候,瀏覽器將按照開發(fā)者定義的這個(gè)規(guī)則,去彈出指定的窗口,顯示指定的內(nèi)容。

在上面的例子中,瀏覽器是一切規(guī)則的執(zhí)行者,開發(fā)者是這些規(guī)則的制定者,而JavaScript只是開發(fā)者向?yàn)g覽器描述這些規(guī)則時(shí)所使用的的語(yǔ)言(否則瀏覽器無法知道開發(fā)者想要在什么情況下做什么事)。假如我們通過以下的語(yǔ)句向?yàn)g覽器描述了一條規(guī)則:

<body>
 <button id="btn">點(diǎn)擊</button>
 <script>
 var button = document.getElementById("btn"); //獲取頁(yè)面上的按鈕
 button.addEventListener("click", function(){ //定義點(diǎn)擊事件
 alert("我被點(diǎn)擊了");
 })
 </script>
</body>

js基礎(chǔ)之事件捕獲與冒泡原理

頁(yè)面上現(xiàn)在有一個(gè)按鈕,我們首先使用原生DOM獲取這個(gè)按鈕,然后使用button.addEventListener(“click”, function(){})這樣的語(yǔ)法向?yàn)g覽器描述了一條規(guī)則:當(dāng)這個(gè)按鈕被點(diǎn)擊(click)時(shí),彈出提示框,顯示“我被點(diǎn)擊了”。用戶點(diǎn)擊按鈕后網(wǎng)頁(yè)就會(huì)出現(xiàn)如下提示:

js基礎(chǔ)之事件捕獲與冒泡原理

瀏覽器把這次“點(diǎn)擊”稱為一個(gè)“事件”?!笆录庇糜诿枋鼋换ミ^程中某些特定的關(guān)鍵點(diǎn)(如點(diǎn)擊、鼠標(biāo)滑動(dòng)、滾輪滾動(dòng)、按下鍵盤、觸屏操作等,每個(gè)操作都對(duì)應(yīng)特定的事件,不過事件也可能與用戶行為無關(guān),比如網(wǎng)頁(yè)加載完畢也是一個(gè)事件)。而瀏覽器處理交互最重要的手段就是基于事件來執(zhí)行開發(fā)者定義好的回調(diào)函數(shù)(如在用戶“點(diǎn)擊按鈕”時(shí)“彈出窗口”,而定義“彈出窗口”行為的就是回調(diào)函數(shù),也就是addEventListener中的function)。

定義完這條規(guī)則,當(dāng)用戶點(diǎn)擊按鈕時(shí),瀏覽器就會(huì)彈出上述窗口了。我們稱“點(diǎn)擊”這個(gè)事件是在這個(gè)按鈕上觸發(fā)的(因?yàn)槲覀兊幕卣{(diào)函數(shù)是綁定在這個(gè)按鈕上的)。

那什么是事件的捕獲與冒泡呢?

事件的捕獲與冒泡

這個(gè)問題與HTML的結(jié)構(gòu)息息相關(guān)。

在前端開發(fā)中,我們使用標(biāo)簽語(yǔ)言HTML來描述網(wǎng)頁(yè)結(jié)構(gòu),如一個(gè)標(biāo)題、一個(gè)段落、一個(gè)表格等,這些網(wǎng)頁(yè)元素描述了網(wǎng)頁(yè)上有哪些需要顯示的內(nèi)容,它們構(gòu)成了整個(gè)網(wǎng)頁(yè)的“骨骼”,通常是一種嵌套的結(jié)構(gòu),比如:

<html>
 <head>
 ... //這是對(duì)網(wǎng)頁(yè)內(nèi)容的元描述
 </head>
 
 <body> //這是網(wǎng)頁(yè)需要渲染的真正內(nèi)容
 <div>
  <h2>標(biāo)題</h2>
  <p>這里是一個(gè)段落</p>
 </div>
 </body>
</html>

上述網(wǎng)頁(yè)結(jié)構(gòu)示意圖如下(在沒有設(shè)置padding等屬性的情況下,子元素通常會(huì)填滿父元素,這里的內(nèi)間距只是為了說明元素的嵌套關(guān)系):

js基礎(chǔ)之事件捕獲與冒泡原理

我們看到,body元素是整個(gè)網(wǎng)頁(yè)的容器,它的內(nèi)部包含了一個(gè)div元素,而div的內(nèi)部又包含了兩個(gè)元素:h2和p。假如我們現(xiàn)在在p的內(nèi)部點(diǎn)擊了一下,那么請(qǐng)問我們有沒有點(diǎn)擊它的外部容器div,以及最外部的body呢?

從瀏覽器的角度來看,我們同時(shí)在點(diǎn)擊這三個(gè)元素。

想要證明這個(gè)結(jié)論非常簡(jiǎn)單,只需要使用addEventListener向div和body各自綁定click事件,如果點(diǎn)擊p時(shí)也會(huì)被觸發(fā),那就說明上面的結(jié)論是正確的。毫無疑問,它們會(huì)被觸發(fā)。

那么問題來了,既然用戶同時(shí)在點(diǎn)擊這三個(gè)元素,瀏覽器應(yīng)該先執(zhí)行哪個(gè)元素定義的回調(diào)函數(shù)呢(由于JavaScript采用單線程模型,執(zhí)行回調(diào)函數(shù)必然有一定的先后順序)?

這個(gè)問題實(shí)際上是在說,對(duì)于嵌套的元素,應(yīng)該從內(nèi)向外還是從外向內(nèi)響應(yīng)事件。瀏覽器之爭(zhēng)的兩大對(duì)立方分別有自己的看法:Netscape公司認(rèn)為應(yīng)當(dāng)由最外層的body首先得到這個(gè)事件,其次是div,最后才是目標(biāo)元素p;而微軟的IE開發(fā)組則認(rèn)為,應(yīng)當(dāng)是內(nèi)部的p首先得到這個(gè)事件,然后是div,最后才是body。在沒有標(biāo)準(zhǔn)約束的情況下,兩者按照自己的想法去設(shè)計(jì)瀏覽器的事件模型,Netscape從外向內(nèi)傳播的模型在業(yè)內(nèi)被稱為事件捕獲模型,而微軟從內(nèi)向外傳播的模型則被稱為事件冒泡模型。

兩個(gè)模型雖然從思路上南轅北轍,但是都可以保證所有綁定的回調(diào)函數(shù)正確觸發(fā)(不過觸發(fā)順序是相反的。如果這個(gè)觸發(fā)順序很重要,那么在當(dāng)時(shí),你的代碼可能只能在一個(gè)瀏覽器中正確運(yùn)行,或者去做惡心的瀏覽器兼容)。不過瀏覽器允許開發(fā)者在事件傳播的過程中阻止事件的繼續(xù)傳播,此時(shí)兩者的差異就變得極其明顯。

假如我們?cè)诙x點(diǎn)擊div元素的回調(diào)函數(shù)時(shí)阻止了事件的傳播:

div.addEventListener("click", function(e){
 ...
 e.stopPropagation(); //阻止事件繼續(xù)傳播
})

這個(gè)代碼會(huì)在兩種模型下產(chǎn)生巨大的差異。在捕獲模型中,由于最外部首先得到該事件,因此body的點(diǎn)擊事件首先被觸發(fā),之后是div的點(diǎn)擊事件。由于阻止了事件傳播,p元素不會(huì)觸發(fā)回調(diào)。而在冒泡模型中則恰恰相反,內(nèi)部的p首先得到該事件,其次才是div,因此觸發(fā)回調(diào)的將是p和div,body因?yàn)槭录]有冒泡上來而無法監(jiān)聽到該事件。同樣的代碼在兩種模型中產(chǎn)生了完全不同的行為,這對(duì)于開發(fā)者來說顯然是不可接受的(兩個(gè)模型都有自己的適用場(chǎng)景,也都有自己的合理性,因此對(duì)于模型的好壞不能一概而論)。

那么后來的國(guó)際標(biāo)準(zhǔn)組織是如何解決這個(gè)沖突的呢?答案就是由開發(fā)者自己選擇。

標(biāo)準(zhǔn)的事件綁定使用addEventListener函數(shù),它接收兩個(gè)必傳參數(shù)和一個(gè)可選參數(shù):必傳的為event(事件名,如"cick")和function(回調(diào)函數(shù)),可選的為useCapture(是否使用捕獲模型,默認(rèn)為false,根據(jù)MDN的接口說明,這里也可以傳入一個(gè)對(duì)象,為本次監(jiān)聽設(shè)置其他參數(shù),詳細(xì)請(qǐng)參考MDN接口文檔 - addEventListener)。

div.addEventListener("click", function(){}, true); //使用捕獲模型

第三個(gè)參數(shù)就是標(biāo)識(shí)開發(fā)者是否需要使用捕獲模型,默認(rèn)為false,也就是默認(rèn)使用微軟的冒泡模型(這是因?yàn)榇蠖鄶?shù)事件都只在最內(nèi)部的元素上觸發(fā),這也間接表明,冒泡模型的普適性更好)。如果開發(fā)者的需求確實(shí)需要使用捕獲模型,可以將第三個(gè)參數(shù)設(shè)置為true。比如下面的例子:

事件捕獲與冒泡的用法

了解了事件捕獲與冒泡的基本原理之后,我們舉個(gè)例子來說明這兩個(gè)模型的基本用法。

假設(shè)有以下的DOM結(jié)構(gòu):

<div id="outer">
 <div id="inner" >
 
 </div>
 </div>

這是兩個(gè)重疊的div,當(dāng)點(diǎn)擊時(shí),兩者都會(huì)響應(yīng)這個(gè)click事件。假如事件綁定如下:

var outer = document.querySelector("#outer");
var inner = document.querySelector("#inner");
 outer.addEventListener("click", function(e){
 alert("來自外部div的消息");
 e.stopPropagation(); //阻止事件向內(nèi)部傳播
 }, true); //使用捕獲模型

 inner.addEventListener("click", function(e){
 alert("來自內(nèi)部div的消息");
 }, true); //使用捕獲模型

頁(yè)面上將只顯示外部彈出的消息,內(nèi)部的事件被e.stopPropagation()攔截了下來,導(dǎo)致事件沒有觸發(fā)。而如果寫成下面的代碼:

var outer = document.querySelector("#outer");
var inner = document.querySelector("#inner");
 outer.addEventListener("click", function(e){
 alert("來自外部div的消息");
 }, false); //使用冒泡模型

 inner.addEventListener("click", function(e){
 alert("來自內(nèi)部div的消息");
 e.stopPropagation(); //阻止事件向外部傳播
 }, false); //使用冒泡模型

這次是只顯示了內(nèi)部的消息,而沒有顯示外部的消息,說明事件在向上冒泡的過程中被阻止了。

注意

如果是在表格中內(nèi)嵌復(fù)選框,希望實(shí)現(xiàn)點(diǎn)擊一行時(shí)選中復(fù)選框,通過stopPropagation阻止CheckBox響應(yīng)click事件并不能實(shí)現(xiàn)。測(cè)試發(fā)現(xiàn)復(fù)選框狀態(tài)改變的事件似乎并不是在click事件觸發(fā)的(斷點(diǎn)跟蹤表明,CheckBox在執(zhí)行click回調(diào)之前,狀態(tài)就已經(jīng)發(fā)生了改變,具體是通過什么事件改變了選中狀態(tài)尚不清楚),下面給一個(gè)可以處理行點(diǎn)擊的示例:

<table border="1" cellspacing="0">
 <tr class="tr">
 <td>
 <input class="checkbox" type="checkbox">
 
 </td>
 <td>
 表格第一行
 </td>
 </tr>
 <tr class="tr">
 <td>
 <input class="checkbox" type="checkbox">
 </td>
 <td>
 表格第二行
 </td>
 </tr>
</table>
<script>
 var tr = document.querySelectorAll(".tr"); //獲取所有tr
 tr.forEach(function(item){ //為每個(gè)tr綁定click事件,手動(dòng)選中復(fù)選框
 item.addEventListener("click", function(e){
 var checkbox = item.querySelector(".checkbox");
 checkbox.checked = !checkbox.checked;
 })
 })

 var cb = document.querySelectorAll(".checkbox");
 cb.forEach(function(item){
 item.addEventListener("click", function(e){
 this.checked = !this.checked;
 });
 })
</script>

這里沒有使用stopPropagation阻止事件傳播,而是通過為CheckBox定義額外的click事件來解決狀態(tài)不變的問題(經(jīng)過斷點(diǎn)跟蹤,此時(shí)在點(diǎn)擊CheckBox時(shí),狀態(tài)發(fā)生了三次變化,第一次是觸發(fā)了某個(gè)原生事件導(dǎo)致其狀態(tài)變化,第二次是執(zhí)行了tr的點(diǎn)擊事件,第三次則是為CheckBox自定義的click事件)。也就是說,點(diǎn)擊tr時(shí)狀態(tài)改變一次,點(diǎn)擊CheckBox時(shí)狀態(tài)改變?nèi)危δ芫!?/p>

由于在大多數(shù)情況下,事件都是由最內(nèi)層的元素來處理的,所以冒泡模型的應(yīng)用更為廣泛,它也因此成為綁定事件時(shí)使用的默認(rèn)模型。

總結(jié)

事件的捕獲與冒泡兩個(gè)模型相對(duì)比較簡(jiǎn)單,只要明白了其中的原理,就可以很容易掌握通過stopPropagation阻止事件傳播的使用。

瀏覽器的標(biāo)準(zhǔn)事件模型把事件的傳播過程分成了三個(gè)階段:捕獲階段、處于目標(biāo)階段和冒泡階段。捕獲階段指事件從最外層傳播到最內(nèi)層之前的整個(gè)過程,對(duì)應(yīng)捕獲模型;處于目標(biāo)階段指的是事件剛好傳播到目標(biāo)元素上;而冒泡階段指的是從最內(nèi)層元素向外傳播的整個(gè)過程。所以我們看到,標(biāo)準(zhǔn)的瀏覽器事件模型就是把捕獲模型和冒泡模型有機(jī)地結(jié)合起來,使開發(fā)者可以以最簡(jiǎn)單的方式靈活地使用兩個(gè)模型。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI