Javascript 冒泡和捕獲

2023-02-17 10:54 更新

讓我們從一個(gè)示例開(kāi)始。

處理程序(handler)被分配給了 <div>,但是如果你點(diǎn)擊任何嵌套的標(biāo)簽(例如 <em> 或 <code>),該處理程序也會(huì)運(yùn)行:

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

這是不是有點(diǎn)奇怪?如果實(shí)際上點(diǎn)擊的是 <em>,為什么在 <div> 上的處理程序會(huì)運(yùn)行?

冒泡

冒泡(bubbling)原理很簡(jiǎn)單。

當(dāng)一個(gè)事件發(fā)生在一個(gè)元素上,它會(huì)首先運(yùn)行在該元素上的處理程序,然后運(yùn)行其父元素上的處理程序,然后一直向上到其他祖先上的處理程序。

假設(shè)我們有 3 層嵌套 FORM > DIV > P,它們各自擁有一個(gè)處理程序:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form onclick="alert('form')">FORM
  <div onclick="alert('div')">DIV
    <p onclick="alert('p')">P</p>
  </div>
</form>


點(diǎn)擊內(nèi)部的 <p> 會(huì)首先運(yùn)行 onclick

  1. 在該 ?<p>? 上的。
  2. 然后是外部 ?<div>? 上的。
  3. 然后是外部 ?<form>? 上的。
  4. 以此類(lèi)推,直到最后的 ?document? 對(duì)象。


因此,如果我們點(diǎn)擊 <p>,那么我們將看到 3 個(gè) alert:p → div → form

這個(gè)過(guò)程被稱為“冒泡(bubbling)”,因?yàn)槭录膬?nèi)部元素“冒泡”到所有父級(jí),就像在水里的氣泡一樣。

幾乎所有事件都會(huì)冒泡。

這句話中的關(guān)鍵詞是“幾乎”。

例如,focus 事件不會(huì)冒泡。同樣,我們以后還會(huì)遇到其他例子。但這仍然是例外,而不是規(guī)則,大多數(shù)事件的確都是冒泡的。

event.target

父元素上的處理程序始終可以獲取事件實(shí)際發(fā)生位置的詳細(xì)信息。

引發(fā)事件的那個(gè)嵌套層級(jí)最深的元素被稱為目標(biāo)元素,可以通過(guò) event.target 訪問(wèn)。

注意與 this(=event.currentTarget)之間的區(qū)別:

  • ?event.target? —— 是引發(fā)事件的“目標(biāo)”元素,它在冒泡過(guò)程中不會(huì)發(fā)生變化。
  • ?this? —— 是“當(dāng)前”元素,其中有一個(gè)當(dāng)前正在運(yùn)行的處理程序。

例如,如果我們有一個(gè)處理程序 form.onclick,那么它可以“捕獲”表單內(nèi)的所有點(diǎn)擊。無(wú)論點(diǎn)擊發(fā)生在哪里,它都會(huì)冒泡到 <form> 并運(yùn)行處理程序。

在 form.onclick 處理程序中:

  • ?this?(=?event.currentTarget?)是 ?<form>? 元素,因?yàn)樘幚沓绦蛟谒厦孢\(yùn)行。
  • ?event.target? 是表單中實(shí)際被點(diǎn)擊的元素。

一探究竟:

  • index.html
  • <!DOCTYPE HTML>
    <html>
    
    <head>
      <meta charset="utf-8">
      <link rel="stylesheet" href="example.css">
    </head>
    
    <body>
      A click shows both <code>event.target</code> and <code>this</code> to compare:
    
      <form id="form">FORM
        <div>DIV
          <p>P</p>
        </div>
      </form>
    
      <script src="script.js"></script>
    </body>
    </html>
  • example.css
  • form {
      background-color: green;
      position: relative;
      width: 150px;
      height: 150px;
      text-align: center;
      cursor: pointer;
    }
    
    div {
      background-color: blue;
      position: absolute;
      top: 25px;
      left: 25px;
      width: 100px;
      height: 100px;
    }
    
    p {
      background-color: red;
      position: absolute;
      top: 25px;
      left: 25px;
      width: 50px;
      height: 50px;
      line-height: 50px;
      margin: 0;
    }
    
    body {
      line-height: 25px;
      font-size: 16px;
    }
  • script.js
  • form.onclick = function(event) {
      event.target.style.backgroundColor = 'yellow';
    
      // chrome needs some time to paint yellow
      setTimeout(() => {
        alert("target = " + event.target.tagName + ", this=" + this.tagName);
        event.target.style.backgroundColor = ''
      }, 0);
    };

event.target 可能會(huì)等于 this —— 當(dāng)點(diǎn)擊事件發(fā)生在 <form> 元素上時(shí),就會(huì)發(fā)生這種情況。

停止冒泡

冒泡事件從目標(biāo)元素開(kāi)始向上冒泡。通常,它會(huì)一直上升到 <html>,然后再到 document 對(duì)象,有些事件甚至?xí)竭_(dá) window,它們會(huì)調(diào)用路徑上所有的處理程序。

但是任意處理程序都可以決定事件已經(jīng)被完全處理,并停止冒泡。

用于停止冒泡的方法是 event.stopPropagation()。

例如,如果你點(diǎn)擊 <button>,這里的 body.onclick 不會(huì)工作:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>

event.stopImmediatePropagation()

如果一個(gè)元素在一個(gè)事件上有多個(gè)處理程序,即使其中一個(gè)停止冒泡,其他處理程序仍會(huì)執(zhí)行。

換句話說(shuō),event.stopPropagation() 停止向上移動(dòng),但是當(dāng)前元素上的其他處理程序都會(huì)繼續(xù)運(yùn)行。

有一個(gè) event.stopImmediatePropagation() 方法,可以用于停止冒泡,并阻止當(dāng)前元素上的處理程序運(yùn)行。使用該方法之后,其他處理程序就不會(huì)被執(zhí)行。

不要在沒(méi)有需要的情況下停止冒泡!

冒泡很方便。不要在沒(méi)有真實(shí)需求時(shí)阻止它:除非是顯而易見(jiàn)的,并且在架構(gòu)上經(jīng)過(guò)深思熟慮的。

有時(shí) event.stopPropagation() 會(huì)產(chǎn)生隱藏的陷阱,以后可能會(huì)成為問(wèn)題。

例如:

  1. 我們創(chuàng)建了一個(gè)嵌套菜單,每個(gè)子菜單各自處理對(duì)自己的元素的點(diǎn)擊事件,并調(diào)用 ?stopPropagation?,以便不會(huì)觸發(fā)外部菜單。
  2. 之后,我們決定捕獲在整個(gè)窗口上的點(diǎn)擊,以追蹤用戶的行為(用戶點(diǎn)擊的位置)。有些分析系統(tǒng)會(huì)這樣做。通常,代碼會(huì)使用 ?document.addEventListener('click'…)? 來(lái)捕獲所有的點(diǎn)擊。
  3. 我們的分析不適用于被 ?stopPropagation? 所阻止點(diǎn)擊的區(qū)域。太傷心了,我們有一個(gè)“死區(qū)”。

通常,沒(méi)有真正的必要去阻止冒泡。一項(xiàng)看似需要阻止冒泡的任務(wù),可以通過(guò)其他方法解決。其中之一就是使用自定義事件,稍后我們會(huì)介紹它們此外,我們還可以將我們的數(shù)據(jù)寫(xiě)入一個(gè)處理程序中的 event 對(duì)象,并在另一個(gè)處理程序中讀取該數(shù)據(jù),這樣我們就可以向父處理程序傳遞有關(guān)下層處理程序的信息。

捕獲

事件處理的另一個(gè)階段被稱為“捕獲(capturing)”。它很少被用在實(shí)際開(kāi)發(fā)中,但有時(shí)是有用的。

DOM 事件標(biāo)準(zhǔn)描述了事件傳播的 3 個(gè)階段:

  1. 捕獲階段(Capturing phase)—— 事件(從 Window)向下走近元素。
  2. 目標(biāo)階段(Target phase)—— 事件到達(dá)目標(biāo)元素。
  3. 冒泡階段(Bubbling phase)—— 事件從元素上開(kāi)始冒泡。

下面是在表格中點(diǎn)擊 <td> 的圖片,摘自規(guī)范:


也就是說(shuō):點(diǎn)擊 <td>,事件首先通過(guò)祖先鏈向下到達(dá)元素(捕獲階段),然后到達(dá)目標(biāo)(目標(biāo)階段),最后上升(冒泡階段),在途中調(diào)用處理程序。

之前,我們只討論了冒泡,因?yàn)椴东@階段很少被使用。通常我們看不到它。

使用 on<event> 屬性或使用 HTML 特性(attribute)或使用兩個(gè)參數(shù)的 addEventListener(event, handler) 添加的處理程序,對(duì)捕獲一無(wú)所知,它們僅在第二階段和第三階段運(yùn)行。

為了在捕獲階段捕獲事件,我們需要將處理程序的 capture 選項(xiàng)設(shè)置為 true

elem.addEventListener(..., {capture: true})
// 或者,用 {capture: true} 的別名 "true"
elem.addEventListener(..., true)

capture 選項(xiàng)有兩個(gè)可能的值:

  • 如果為 ?false?(默認(rèn)值),則在冒泡階段設(shè)置處理程序。
  • 如果為 ?true?,則在捕獲階段設(shè)置處理程序。

請(qǐng)注意,雖然形式上有 3 個(gè)階段,但第 2 階段(“目標(biāo)階段”:事件到達(dá)元素)沒(méi)有被單獨(dú)處理:捕獲階段和冒泡階段的處理程序都在該階段被觸發(fā)。

讓我們來(lái)看看捕獲和冒泡:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

上面這段代碼為文檔中的 每個(gè) 元素都設(shè)置了點(diǎn)擊處理程序,以查看哪些元素上的點(diǎn)擊事件處理程序生效了。

如果你點(diǎn)擊了 <p>,那么順序是:

  1. ?HTML? → ?BODY? → ?FORM? → ?DIV?(捕獲階段第一個(gè)監(jiān)聽(tīng)器):
  2. ?P?(目標(biāo)階段,觸發(fā)兩次,因?yàn)槲覀冊(cè)O(shè)置了兩個(gè)監(jiān)聽(tīng)器:捕獲和冒泡)
  3. ?DIV? → ?FORM? → ?BODY? → ?HTML?(冒泡階段,第二個(gè)監(jiān)聽(tīng)器)。

有一個(gè)屬性 event.eventPhase,它告訴我們捕獲事件的階段數(shù)。但它很少被使用,因?yàn)槲覀兺ǔJ菑奶幚沓绦蛑辛私獾剿?

要移除處理程序,?removeEventListener? 需要同一階段

如果我們 addEventListener(..., true),那么我們應(yīng)該在 removeEventListener(..., true) 中提到同一階段,以正確刪除處理程序。

同一元素的同一階段的監(jiān)聽(tīng)器按其設(shè)置順序運(yùn)行

如果我們?cè)谕浑A段有多個(gè)事件處理程序,并通過(guò) addEventListener 分配給了相同的元素,則它們的運(yùn)行順序與創(chuàng)建順序相同:

elem.addEventListener("click", e => alert(1)); // 會(huì)先被觸發(fā)
elem.addEventListener("click", e => alert(2));

總結(jié)

當(dāng)一個(gè)事件發(fā)生時(shí) —— 發(fā)生該事件的嵌套最深的元素被標(biāo)記為“目標(biāo)元素”(?event.target?)。

  • 然后,事件從文檔根節(jié)點(diǎn)向下移動(dòng)到 ?event.target?,并在途中調(diào)用分配了 ?addEventListener(..., true)? 的處理程序(?true? 是 ?{capture: true}? 的一個(gè)簡(jiǎn)寫(xiě)形式)。
  • 然后,在目標(biāo)元素自身上調(diào)用處理程序。
  • 然后,事件從 ?event.target? 冒泡到根,調(diào)用使用 ?on<event>?、HTML 特性(attribute)和沒(méi)有第三個(gè)參數(shù)的,或者第三個(gè)參數(shù)為 ?false/{capture:false}? 的 ?addEventListener? 分配的處理程序。

每個(gè)處理程序都可以訪問(wèn) event 對(duì)象的屬性:

  • ?event.target? —— 引發(fā)事件的層級(jí)最深的元素。
  • ?event.currentTarget?(=?this?)—— 處理事件的當(dāng)前元素(具有處理程序的元素)
  • ?event.eventPhase? —— 當(dāng)前階段(capturing=1,target=2,bubbling=3)。

任何事件處理程序都可以通過(guò)調(diào)用 event.stopPropagation() 來(lái)停止事件,但不建議這樣做,因?yàn)槲覀儾淮_定是否確實(shí)不需要冒泡上來(lái)的事件,也許是用于完全不同的事情。

捕獲階段很少使用,通常我們會(huì)在冒泡時(shí)處理事件。這背后有一個(gè)邏輯。

在現(xiàn)實(shí)世界中,當(dāng)事故發(fā)生時(shí),當(dāng)?shù)鼐綍?huì)首先做出反應(yīng)。他們最了解發(fā)生這件事的地方。然后,如果需要,上級(jí)主管部門(mén)再進(jìn)行處理。

事件處理程序也是如此。在特定元素上設(shè)置處理程序的代碼,了解有關(guān)該元素最詳盡的信息。特定于 <td> 的處理程序可能恰好適合于該 <td>,這個(gè)處理程序知道關(guān)于該元素的所有信息。所以該處理程序應(yīng)該首先獲得機(jī)會(huì)。然后,它的直接父元素也了解相關(guān)上下文,但了解的內(nèi)容會(huì)少一些,以此類(lèi)推,直到處理一般性概念并運(yùn)行最后一個(gè)處理程序的最頂部的元素為止。

冒泡和捕獲為“事件委托”奠定了基礎(chǔ) —— 一種非常強(qiáng)大的事件處理模式,我們將在下一章中進(jìn)行研究。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)