2022 年 10 月 14 日

冒泡和捕獲

讓我們從一個範例開始。

這個處理函式指定給 <div>,但如果您按一下任何巢狀標籤,例如 <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> 上的處理函式會執行?

冒泡

冒泡原理很簡單。

當某個元素發生事件時,它會先執行該元素上的處理函式,然後執行其父元素上的處理函式,然後一路執行到其他祖先元素上的處理函式。

假設我們有 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>

按一下內層 <p> 會先執行 onclick

  1. 在那個 <p> 上。
  2. 然後在外部 <div> 上。
  3. 然後在外部 <form> 上。
  4. 然後向上到 document 物件。

所以如果我們按一下 <p>,我們就會看到 3 個快訊:pdivform

這個程序稱為「冒泡」,因為事件會像水中的泡泡一樣從內部元素冒泡到父元素。

幾乎 所有事件都會冒泡。

這句話中的關鍵字是「幾乎」。

例如,focus 事件不會冒泡。還有其他範例,我們會遇到它們。但它仍然是一個例外,而不是一個規則,大多數事件都會冒泡。

event.target

父元素上的處理常式可以隨時取得事件實際發生的詳細資料。

導致事件的最深層巢狀元素稱為目標元素,可作為 event.target 存取。

注意與 this (=event.currentTarget) 的差異

  • event.target – 是啟動事件的「目標」元素,它不會在冒泡程序中改變。
  • this – 是「目前的」元素,也就是目前正在執行處理常式的元素。

例如,如果我們有一個單一處理常式 form.onclick,那麼它可以「捕捉」表單內的所有按一下動作。無論按一下動作發生在哪裡,它都會冒泡到 <form> 並執行處理常式。

form.onclick 處理常式中

  • this (=event.currentTarget) 是 <form> 元素,因為處理常式在它上面執行。
  • event.target 是表單內實際按一下的元素。

查看它

結果
script.js
example.css
index.html
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);
};
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;
}
<!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>

event.target 可能會等於 this – 當按一下動作直接在 <form> 元素上執行時會發生這種情況。

停止冒泡

冒泡事件會從目標元素直接向上傳遞。通常它會向上傳遞到 <html>,然後傳遞到 document 物件,有些事件甚至會到達 window,呼叫路徑上的所有處理常式。

但任何處理常式都可以決定事件已完全處理完畢並停止冒泡。

它的方法是 event.stopPropagation()

例如,如果按一下 <button>,這裡的 body.onclick 就不會作用

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

如果一個元素在單一事件上有多個事件處理函式,則即使其中一個停止冒泡,其他函式仍會執行。

換句話說,event.stopPropagation() 會停止向上移動,但所有其他處理函式仍會在目前的元素上執行。

若要停止冒泡並防止目前元素上的處理函式執行,有一個方法 event.stopImmediatePropagation()。在它之後,不會再執行其他處理函式。

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

冒泡很方便。不要在沒有真正需要的情況下停止它:顯而易見且經過周詳架構設計。

有時 event.stopPropagation() 會造成隱藏的陷阱,之後可能會變成問題。

例如

  1. 我們建立一個巢狀選單。每個子選單會處理其元素上的點擊,並呼叫 stopPropagation,這樣外部選單就不會觸發。
  2. 稍後我們決定捕捉整個視窗上的點擊,以追蹤使用者的行為(人們點擊的位置)。有些分析系統會這麼做。程式碼通常會使用 document.addEventListener('click'…) 來捕捉所有點擊。
  3. 我們的分析系統無法在 stopPropagation 停止點擊的區域上運作。很遺憾,我們有一個「死角」。

通常沒有真正的需要來防止冒泡。看似需要這樣做的任務可以用其他方式解決。其中一種方式是使用自訂事件,我們稍後會介紹。我們也可以在一個處理函式中將資料寫入 event 物件,並在另一個處理函式中讀取它,這樣我們就可以將有關下方處理程序的資訊傳遞給父層的處理函式。

擷取

事件處理的另一個階段稱為「擷取」。它在實際程式碼中很少使用,但有時可能很有用。

標準的 DOM 事件 描述了事件傳播的 3 個階段

  1. 擷取階段 - 事件傳遞到元素。
  2. 目標階段 - 事件到達目標元素。
  3. 冒泡階段 - 事件從元素冒泡上來。

以下是從規格中擷取的圖片,顯示了表格內 <td> 上點擊事件的擷取 (1)、目標 (2) 和冒泡 (3) 階段

換句話說:對於在 <td> 上的點擊,事件首先會穿過祖先鏈,向下傳遞到元素(捕獲階段),然後到達目標並在那裡觸發(目標階段),然後向上傳遞(冒泡階段),並在傳遞過程中呼叫處理常式。

到目前為止,我們只討論了冒泡,因為捕獲階段很少使用。

事實上,捕獲階段對我們來說是看不見的,因為使用 on<event> 屬性或使用 HTML 屬性或使用雙參數 addEventListener(event, handler) 新增的處理常式對捕獲一無所知,它們只在第 2 和第 3 階段執行。

要在捕獲階段捕獲事件,我們需要將處理常式 capture 選項設定為 true

elem.addEventListener(..., {capture: true})

// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

capture 選項有兩個可能的值

  • 如果它是 false(預設值),則處理常式會在冒泡階段設定。
  • 如果它是 true,則處理常式會在捕獲階段設定。

請注意,雖然正式上有 3 個階段,但第 2 階段(「目標階段」:事件到達元素)不會單獨處理:捕獲和冒泡階段的處理常式都會在該階段觸發。

讓我們看看捕獲和冒泡的實際運作

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

該程式碼會在文件中的每個元素上設定點擊處理常式,以查看哪些處理常式有效。

如果您點擊 <p>,則順序為

  1. HTMLBODYFORMDIV -> P(捕獲階段,第一個偵聽器)
  2. PDIVFORMBODYHTML(冒泡階段,第二個偵聽器)。

請注意,P 出現兩次,因為我們設定了兩個偵聽器:捕獲和冒泡。目標在第一階段結束時和第二階段開始時觸發。

有一個屬性 event.eventPhase,它告訴我們事件被捕獲的階段數。但它很少使用,因為我們通常在處理常式中知道它。

要移除處理常式,removeEventListener 需要相同的階段

如果我們 addEventListener(..., true),則我們應該在 removeEventListener(..., true) 中提到相同的階段,以正確移除處理常式。

在同一元素和同一階段上的偵聽器會按照設定順序執行

如果我們在同一階段有多個事件處理常式,並使用 addEventListener 指定給同一元素,它們會按照建立順序執行

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
捕獲期間的 event.stopPropagation() 也會阻止冒泡

event.stopPropagation() 方法及其同級 event.stopImmediatePropagation() 也可以在捕獲階段呼叫。然後,不僅進一步的捕獲會停止,而且冒泡也會停止。

換句話說,事件通常會先向下傳遞(「捕獲」),然後向上傳遞(「冒泡」)。但是,如果在捕獲階段呼叫 event.stopPropagation(),則事件傳遞會停止,不會發生冒泡。

摘要

當事件發生時,最內層的元素會被標記為「目標元素」(event.target)。

  • 然後事件會從文件根目錄移動到 event.target,呼叫在途中使用 addEventListener(..., true) 指定的處理常式 (true{capture: true} 的簡寫)。
  • 然後在目標元素本身呼叫處理常式。
  • 然後事件會從 event.target 冒泡到根目錄,呼叫使用 on<event>、HTML 屬性和 addEventListener 指定的處理常式,而沒有第三個參數或第三個參數為 false/{capture:false}

每個處理常式都可以存取 event 物件屬性

  • event.target – 產生事件的最深層元素。
  • event.currentTarget (=this) – 處理事件的目前元素 (有處理常式的元素)
  • event.eventPhase – 目前階段 (擷取=1,目標=2,冒泡=3)。

任何事件處理常式都可以透過呼叫 event.stopPropagation() 來停止事件,但這不建議這樣做,因為我們無法確定在上面是否需要它,也許是完全不同的事情。

擷取階段很少使用,我們通常在冒泡時處理事件。這是有邏輯解釋的。

在現實世界中,當事故發生時,地方當局會首先做出反應。他們最了解發生事故的地區。然後在必要時由上級機關處理。

事件處理常式也是如此。在特定元素上設定處理常式的程式碼會知道關於元素和它所做事情的最詳細資訊。特定 <td> 上的處理常式可能完全適合該 <td>,它知道關於它的所有事情,所以它應該先得到機會。然後它的直接父元素也了解這個背景,但了解得少一點,以此類推,直到處理一般概念並執行最後一個動作的最頂層元素。

冒泡和擷取奠定了「事件委派」的基礎,這是一種極為強大的事件處理模式,我們將在下一個章節中學習。

教學地圖

留言

留言前請先閱讀…
  • 如果您有建議要改進 - 請 提交 GitHub 問題 或提交拉取請求,而不是發表評論。
  • 如果您無法理解文章中的某些內容 - 請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤,若要插入多行,請將它們包在 <pre> 標籤中,若要插入 10 行以上的程式碼,請使用沙盒 (plnkrjsbincodepen…)