Javascript 冒泡和捕獲

2023-02-17 10:54 更新

讓我們從一個示例開始。

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

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

這是不是有點奇怪?如果實際上點擊的是 <em>,為什么在 <div> 上的處理程序會運行?

冒泡

冒泡(bubbling)原理很簡單。

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

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

<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>


點擊內(nèi)部的 <p> 會首先運行 onclick

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


因此,如果我們點擊 <p>,那么我們將看到 3 個 alert:p → div → form。

這個過程被稱為“冒泡(bubbling)”,因為事件從內(nèi)部元素“冒泡”到所有父級,就像在水里的氣泡一樣。

幾乎所有事件都會冒泡。

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

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

event.target

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

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

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

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

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

在 form.onclick 處理程序中:

  • ?this?(=?event.currentTarget?)是 ?<form>? 元素,因為處理程序在它上面運行。
  • ?event.target? 是表單中實際被點擊的元素。

一探究竟:

  • 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 可能會等于 this —— 當(dāng)點擊事件發(fā)生在 <form> 元素上時,就會發(fā)生這種情況。

停止冒泡

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

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

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

例如,如果你點擊 <button>,這里的 body.onclick 不會工作:

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

event.stopImmediatePropagation()

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

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

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

不要在沒有需要的情況下停止冒泡!

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

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

例如:

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

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

捕獲

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

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

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

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


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

之前,我們只討論了冒泡,因為捕獲階段很少被使用。通常我們看不到它。

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

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

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

capture 選項有兩個可能的值:

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

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

讓我們來看看捕獲和冒泡:

<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>

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

如果你點擊了 <p>,那么順序是:

  1. ?HTML? → ?BODY? → ?FORM? → ?DIV?(捕獲階段第一個監(jiān)聽器):
  2. ?P?(目標(biāo)階段,觸發(fā)兩次,因為我們設(shè)置了兩個監(jiān)聽器:捕獲和冒泡)
  3. ?DIV? → ?FORM? → ?BODY? → ?HTML?(冒泡階段,第二個監(jiān)聽器)。

有一個屬性 event.eventPhase,它告訴我們捕獲事件的階段數(shù)。但它很少被使用,因為我們通常是從處理程序中了解到它。

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

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

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

如果我們在同一階段有多個事件處理程序,并通過 addEventListener 分配給了相同的元素,則它們的運行順序與創(chuàng)建順序相同:

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

總結(jié)

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

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

每個處理程序都可以訪問 event 對象的屬性:

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

任何事件處理程序都可以通過調(diào)用 event.stopPropagation() 來停止事件,但不建議這樣做,因為我們不確定是否確實不需要冒泡上來的事件,也許是用于完全不同的事情。

捕獲階段很少使用,通常我們會在冒泡時處理事件。這背后有一個邏輯。

在現(xiàn)實世界中,當(dāng)事故發(fā)生時,當(dāng)?shù)鼐綍紫茸龀龇磻?yīng)。他們最了解發(fā)生這件事的地方。然后,如果需要,上級主管部門再進(jìn)行處理。

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號