js基礎(chǔ)之事件捕獲與冒泡原理
想要了解什么是事件捕獲與冒泡,需要先了解什么是事件。
什么是事件?
我們知道,在前端開發(fā)中,JavaScript負責定義網(wǎng)頁的“行為”。這里所說的“定義”,其實指的是開發(fā)者可以通過JavaScript語言向瀏覽器描述一些規(guī)則,瀏覽器按照這些規(guī)則與用戶進行交互。比如開發(fā)者希望當用戶點擊頁面上某個按鈕的時候,就彈出一個窗口,顯示特定的內(nèi)容。而當用戶真正點擊這個按鈕的時候,瀏覽器將按照開發(fā)者定義的這個規(guī)則,去彈出指定的窗口,顯示指定的內(nèi)容。
在上面的例子中,瀏覽器是一切規(guī)則的執(zhí)行者,開發(fā)者是這些規(guī)則的制定者,而JavaScript只是開發(fā)者向瀏覽器描述這些規(guī)則時所使用的的語言(否則瀏覽器無法知道開發(fā)者想要在什么情況下做什么事)。假如我們通過以下的語句向瀏覽器描述了一條規(guī)則:
<body> <button id="btn">點擊</button> <script> var button = document.getElementById("btn"); //獲取頁面上的按鈕 button.addEventListener("click", function(){ //定義點擊事件 alert("我被點擊了"); }) </script> </body>
頁面上現(xiàn)在有一個按鈕,我們首先使用原生DOM獲取這個按鈕,然后使用button.addEventListener(“click”, function(){})這樣的語法向瀏覽器描述了一條規(guī)則:當這個按鈕被點擊(click)時,彈出提示框,顯示“我被點擊了”。用戶點擊按鈕后網(wǎng)頁就會出現(xiàn)如下提示:
瀏覽器把這次“點擊”稱為一個“事件”?!笆录庇糜诿枋鼋换ミ^程中某些特定的關(guān)鍵點(如點擊、鼠標滑動、滾輪滾動、按下鍵盤、觸屏操作等,每個操作都對應(yīng)特定的事件,不過事件也可能與用戶行為無關(guān),比如網(wǎng)頁加載完畢也是一個事件)。而瀏覽器處理交互最重要的手段就是基于事件來執(zhí)行開發(fā)者定義好的回調(diào)函數(shù)(如在用戶“點擊按鈕”時“彈出窗口”,而定義“彈出窗口”行為的就是回調(diào)函數(shù),也就是addEventListener中的function)。
定義完這條規(guī)則,當用戶點擊按鈕時,瀏覽器就會彈出上述窗口了。我們稱“點擊”這個事件是在這個按鈕上觸發(fā)的(因為我們的回調(diào)函數(shù)是綁定在這個按鈕上的)。
那什么是事件的捕獲與冒泡呢?
事件的捕獲與冒泡
這個問題與HTML的結(jié)構(gòu)息息相關(guān)。
在前端開發(fā)中,我們使用標簽語言HTML來描述網(wǎng)頁結(jié)構(gòu),如一個標題、一個段落、一個表格等,這些網(wǎng)頁元素描述了網(wǎng)頁上有哪些需要顯示的內(nèi)容,它們構(gòu)成了整個網(wǎng)頁的“骨骼”,通常是一種嵌套的結(jié)構(gòu),比如:
<html> <head> ... //這是對網(wǎng)頁內(nèi)容的元描述 </head> <body> //這是網(wǎng)頁需要渲染的真正內(nèi)容 <div> <h1>標題</h1> <p>這里是一個段落</p> </div> </body> </html>
上述網(wǎng)頁結(jié)構(gòu)示意圖如下(在沒有設(shè)置padding等屬性的情況下,子元素通常會填滿父元素,這里的內(nèi)間距只是為了說明元素的嵌套關(guān)系):
我們看到,body元素是整個網(wǎng)頁的容器,它的內(nèi)部包含了一個div元素,而div的內(nèi)部又包含了兩個元素:h1和p。假如我們現(xiàn)在在p的內(nèi)部點擊了一下,那么請問我們有沒有點擊它的外部容器div,以及最外部的body呢?
從瀏覽器的角度來看,我們同時在點擊這三個元素。
想要證明這個結(jié)論非常簡單,只需要使用addEventListener向div和body各自綁定click事件,如果點擊p時也會被觸發(fā),那就說明上面的結(jié)論是正確的。毫無疑問,它們會被觸發(fā)。
那么問題來了,既然用戶同時在點擊這三個元素,瀏覽器應(yīng)該先執(zhí)行哪個元素定義的回調(diào)函數(shù)呢(由于JavaScript采用單線程模型,執(zhí)行回調(diào)函數(shù)必然有一定的先后順序)?
這個問題實際上是在說,對于嵌套的元素,應(yīng)該從內(nèi)向外還是從外向內(nèi)響應(yīng)事件。瀏覽器之爭的兩大對立方分別有自己的看法:Netscape公司認為應(yīng)當由最外層的body首先得到這個事件,其次是div,最后才是目標元素p;而微軟的IE開發(fā)組則認為,應(yīng)當是內(nèi)部的p首先得到這個事件,然后是div,最后才是body。在沒有標準約束的情況下,兩者按照自己的想法去設(shè)計瀏覽器的事件模型,Netscape從外向內(nèi)傳播的模型在業(yè)內(nèi)被稱為事件捕獲模型,而微軟從內(nèi)向外傳播的模型則被稱為事件冒泡模型。
兩個模型雖然從思路上南轅北轍,但是都可以保證所有綁定的回調(diào)函數(shù)正確觸發(fā)(不過觸發(fā)順序是相反的。如果這個觸發(fā)順序很重要,那么在當時,你的代碼可能只能在一個瀏覽器中正確運行,或者去做惡心的瀏覽器兼容)。不過瀏覽器允許開發(fā)者在事件傳播的過程中阻止事件的繼續(xù)傳播,此時兩者的差異就變得極其明顯。
假如我們在定義點擊div元素的回調(diào)函數(shù)時阻止了事件的傳播:
div.addEventListener("click", function(e){ ... e.stopPropagation(); //阻止事件繼續(xù)傳播 })
這個代碼會在兩種模型下產(chǎn)生巨大的差異。在捕獲模型中,由于最外部首先得到該事件,因此body的點擊事件首先被觸發(fā),之后是div的點擊事件。由于阻止了事件傳播,p元素不會觸發(fā)回調(diào)。而在冒泡模型中則恰恰相反,內(nèi)部的p首先得到該事件,其次才是div,因此觸發(fā)回調(diào)的將是p和div,body因為事件沒有冒泡上來而無法監(jiān)聽到該事件。同樣的代碼在兩種模型中產(chǎn)生了完全不同的行為,這對于開發(fā)者來說顯然是不可接受的(兩個模型都有自己的適用場景,也都有自己的合理性,因此對于模型的好壞不能一概而論)。
那么后來的國際標準組織是如何解決這個沖突的呢?答案就是由開發(fā)者自己選擇。
標準的事件綁定使用addEventListener函數(shù),它接收兩個必傳參數(shù)和一個可選參數(shù):必傳的為event(事件名,如"cick")和function(回調(diào)函數(shù)),可選的為useCapture(是否使用捕獲模型,默認為false,根據(jù)MDN的接口說明,這里也可以傳入一個對象,為本次監(jiān)聽設(shè)置其他參數(shù),詳細請參考MDN接口文檔 - addEventListener)。
div.addEventListener("click", function(){}, true); //使用捕獲模型
第三個參數(shù)就是標識開發(fā)者是否需要使用捕獲模型,默認為false,也就是默認使用微軟的冒泡模型(這是因為大多數(shù)事件都只在最內(nèi)部的元素上觸發(fā),這也間接表明,冒泡模型的普適性更好)。如果開發(fā)者的需求確實需要使用捕獲模型,可以將第三個參數(shù)設(shè)置為true。比如下面的例子:
事件捕獲與冒泡的用法
了解了事件捕獲與冒泡的基本原理之后,我們舉個例子來說明這兩個模型的基本用法。
假設(shè)有以下的DOM結(jié)構(gòu):
<div id="outer"> <div id="inner" style="width:100px;height: 100px;border: 1px solid black;"> </div> </div>
這是兩個重疊的div,當點擊時,兩者都會響應(yīng)這個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); //使用捕獲模型
頁面上將只顯示外部彈出的消息,內(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ù)選框,希望實現(xiàn)點擊一行時選中復(fù)選框,通過stopPropagation阻止CheckBox響應(yīng)click事件并不能實現(xiàn)。測試發(fā)現(xiàn)復(fù)選框狀態(tài)改變的事件似乎并不是在click事件觸發(fā)的(斷點跟蹤表明,CheckBox在執(zhí)行click回調(diào)之前,狀態(tài)就已經(jīng)發(fā)生了改變,具體是通過什么事件改變了選中狀態(tài)尚不清楚),下面給一個可以處理行點擊的示例:
<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){ //為每個tr綁定click事件,手動選中復(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)過斷點跟蹤,此時在點擊CheckBox時,狀態(tài)發(fā)生了三次變化,第一次是觸發(fā)了某個原生事件導(dǎo)致其狀態(tài)變化,第二次是執(zhí)行了tr的點擊事件,第三次則是為CheckBox自定義的click事件)。也就是說,點擊tr時狀態(tài)改變一次,點擊CheckBox時狀態(tài)改變?nèi)?,功能均正?!?/p>
由于在大多數(shù)情況下,事件都是由最內(nèi)層的元素來處理的,所以冒泡模型的應(yīng)用更為廣泛,它也因此成為綁定事件時使用的默認模型。
總結(jié)
事件的捕獲與冒泡兩個模型相對比較簡單,只要明白了其中的原理,就可以很容易掌握通過stopPropagation阻止事件傳播的使用。
瀏覽器的標準事件模型把事件的傳播過程分成了三個階段:捕獲階段、處于目標階段和冒泡階段。捕獲階段指事件從最外層傳播到最內(nèi)層之前的整個過程,對應(yīng)捕獲模型;處于目標階段指的是事件剛好傳播到目標元素上;而冒泡階段指的是從最內(nèi)層元素向外傳播的整個過程。所以我們看到,標準的瀏覽器事件模型就是把捕獲模型和冒泡模型有機地結(jié)合起來,使開發(fā)者可以以最簡單的方式靈活地使用兩個模型。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JavaScript中Object和Function的關(guān)系小結(jié)
JavaScript 中 Object 和 Function 的關(guān)系是微妙的,他們互為對方的一個實例。2009-09-09Javascript insertAfter() 實現(xiàn)函數(shù)代碼
DOM沒有提供insertAfter()方法,我們可以自己擴展下。2011-10-10javascript簡單實現(xiàn)類似QQ頭像彈出效果的方法
這篇文章主要介紹了javascript簡單實現(xiàn)類似QQ頭像彈出效果的方法,可實現(xiàn)簡單的頁面元素彈出效果,具有一定參考借鑒價值,需要的朋友可以參考下2015-08-08elementui-樹形控件實現(xiàn)子節(jié)點右側(cè)添加圖標和數(shù)據(jù)鼠標放上去顯示文字效果
這篇文章主要介紹了elementui-樹形控件實現(xiàn)子節(jié)點右側(cè)添加圖標和數(shù)據(jù)鼠標放上去顯示文字效果,本文結(jié)合實例代碼給大家介紹的非常詳細,感興趣的朋友一起看看吧2024-01-01