2022 年 2 月 3 日

事件委派

擷取和浮現讓我們得以實作其中一種最強大的事件處理模式,稱為「事件委派」。

其概念是,如果我們有許多元素以類似的方式處理,那麼我們會將單一處理常式放在它們的共同祖先上,而不是為每個元素指定一個處理常式。

在處理常式中,我們會取得 event.target 來查看事件實際發生在哪裡,並加以處理。

讓我們來看一個範例,八卦圖反映了古老的中國哲學。

如下所示

HTML 如下所示

<table>
  <tr>
    <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
  </tr>
  <tr>
    <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
    <td class="n">...</td>
    <td class="ne">...</td>
  </tr>
  <tr>...2 more lines of this kind...</tr>
  <tr>...2 more lines of this kind...</tr>
</table>

表格有 9 個儲存格,但可以有 99 或 9999 個,這並不重要。

我們的任務是點擊時突顯一個儲存格 <td>

我們不會為每個 <td>(可能很多)指定一個 onclick 處理常式,而是會在 <table> 元素上設定「全域」處理常式。

它會使用 event.target 來取得被點擊的元素並突顯它。

程式碼

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // where was the click?

  if (target.tagName != 'TD') return; // not on TD? Then we're not interested

  highlight(target); // highlight it
};

function highlight(td) {
  if (selectedTd) { // remove the existing highlight if any
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // highlight the new td
}

這樣的程式碼不關心表格中有多少個儲存格。我們可以隨時動態新增/移除 <td>,突顯功能仍然會正常運作。

不過,還是有一個缺點。

點擊可能不是發生在 <td> 上,而是在其內部。

在我們的案例中,如果我們查看 HTML 內部,我們可以看到 <td> 內部有巢狀標籤,例如 <strong>

<td>
  <strong>Northwest</strong>
  ...
</td>

自然地,如果點擊發生在那個 <strong> 上,那麼它就會變成 event.target 的值。

在處理常式 table.onclick 中,我們應該取得這樣的 event.target,並找出點擊是否發生在 <td> 內部。

以下是改良後的程式碼

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

說明

  1. 方法 elem.closest(selector) 會傳回符合選擇器的最近祖先。在我們的案例中,我們從來源元素向上尋找 <td>
  2. 如果 event.target 不在任何 <td> 內部,那麼呼叫會立即傳回,因為沒有任何事要做。
  3. 在巢狀表格的情況下,event.target 可能是一個 <td>,但位於目前表格外部。因此,我們會檢查它是否實際上是我們表格的 <td>
  4. 如果是的話,就突顯它。

結果,我們有一個快速、有效率的突顯程式碼,它不關心表格中 <td> 的總數。

委派範例:標記中的動作

事件委派還有其他用途。

假設我們想要建立一個包含「儲存」、「載入」、「搜尋」等按鈕的選單。而且有一個包含 saveloadsearch… 等方法的物件。要如何比對它們?

第一個想法可能是為每個按鈕指定一個獨立的處理常式。但有一個更優雅的解決方案。我們可以為整個選單新增一個處理常式,並為具有要呼叫方法的按鈕新增 data-action 屬性

<button data-action="save">Click to Save</button>

處理常式會讀取屬性並執行方法。查看運作範例

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.onclick = this.onClick.bind(this); // (*)
    }

    save() {
      alert('saving');
    }

    load() {
      alert('loading');
    }

    search() {
      alert('searching');
    }

    onClick(event) {
      let action = event.target.dataset.action;
      if (action) {
        this[action]();
      }
    };
  }

  new Menu(menu);
</script>

請注意,this.onClick(*) 中繫結到 this。這很重要,因為否則其內的 this 會參照 DOM 元素(elem),而不是 Menu 物件,而 this[action] 就會不是我們需要的東西。

那麼,委派在此處帶給我們什麼優勢?

  • 我們不需要撰寫程式碼來為每個按鈕指定處理常式。只要建立一個方法並將其放入標記中即可。
  • HTML 結構很靈活,我們可以隨時新增/移除按鈕。

我們也可以使用類別 .action-save.action-load,但屬性 data-action 在語意上會更好。我們也可以在 CSS 規則中使用它。

「行為」模式

我們也可以使用事件委派來為元素新增「行為」,並使用特殊屬性和類別以「宣告式」的方式進行。

此模式包含兩個部分

  1. 我們為元素新增一個自訂屬性來描述其行為。
  2. 一個文件層級的處理常式會追蹤事件,如果事件發生在具有屬性的元素上,則會執行動作。

行為:計數器

例如,這裡的屬性 data-counter 會為按鈕新增一個行為:「按一下就增加值」

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // if the attribute exists...
      event.target.value++;
    }

  });
</script>

如果我們按一下按鈕,其值就會增加。這裡重要的是按鈕,而不是一般的方法。

可以有任意數量的屬性具有 data-counter。我們可以隨時在 HTML 中新增新的屬性。使用事件委派,我們「擴充」了 HTML,新增了一個屬性來描述新的行為。

對於文件層級的處理常式,永遠都使用 addEventListener

當我們為 document 物件指定事件處理常式時,我們應該永遠使用 addEventListener,而不是 document.on<event>,因為後者會造成衝突:新的處理常式會覆寫舊的處理常式。

對於實際專案,由程式碼的不同部分設定在 document 上的處理常式很多,這是很正常的。

行為:切換器

另一個行為的範例。按一下具有屬性 data-toggle-id 的元素會顯示/隱藏具有指定 id 的元素

<button data-toggle-id="subscribe-mail">
  Show the subscription form
</button>

<form id="subscribe-mail" hidden>
  Your mail: <input type="email">
</form>

<script>
  document.addEventListener('click', function(event) {
    let id = event.target.dataset.toggleId;
    if (!id) return;

    let elem = document.getElementById(id);

    elem.hidden = !elem.hidden;
  });
</script>

讓我們再次注意我們所做的。現在,要為元素新增切換功能,不需要知道 JavaScript,只要使用屬性 data-toggle-id 即可。

這可能會變得非常方便,不需要為每個這樣的元素撰寫 JavaScript。只要使用行為即可。文件層級的處理常式會讓它對頁面的任何元素都起作用。

我們也可以在單一元素上結合多個行為。

「行為」模式可以作為 JavaScript 小型片段的替代方案。

摘要

事件委派真的很酷!它是 DOM 事件中最有用的模式之一。

它通常用於為許多類似的元素添加相同的處理,但不僅於此。

演算法

  1. 在容器上放置一個單一處理常式。
  2. 在處理常式中 - 檢查來源元素 event.target
  3. 如果事件發生在我們感興趣的元素內部,則處理事件。

好處

  • 簡化初始化並節省記憶體:無需添加許多處理常式。
  • 更少的程式碼:在新增或移除元素時,無需新增/移除處理常式。
  • DOM 修改:我們可以使用 innerHTML 等大量新增/移除元素。

當然,委派有其限制

  • 首先,事件必須是冒泡的。有些事件不會冒泡。此外,低階處理常式不應使用 event.stopPropagation()
  • 其次,委派可能會增加 CPU 負載,因為容器層級處理常式會對容器中任何地方的事件做出反應,無論我們是否感興趣。但通常負載可以忽略不計,因此我們不考慮它。

任務

重要性:5

有一個訊息清單,其中包含移除按鈕 [x]。讓按鈕發揮作用。

像這樣

附註:容器上只應有一個事件監聽器,請使用事件委派。

為任務開啟沙盒。

重要性:5

建立一個樹狀結構,在按一下時顯示/隱藏節點子節點

需求

  • 只有一個事件處理常式(使用委派)
  • 在節點標題外部(在空白處)按一下不應執行任何動作。

為任務開啟沙盒。

解決方案分為兩部分。

  1. 將每個樹狀節點標題包覆在 <span> 中。然後我們可以在 :hover 上使用 CSS 為它們設定樣式,並準確地處理文字上的按一下,因為 <span> 寬度與文字寬度完全相同(與沒有它時不同)。
  2. 將處理常式設定為 tree 根節點,並處理 <span> 標題上的按一下。

在沙盒中開啟解決方案。

重要性:4

讓表格可排序:點擊 <th> 元素應按對應的欄位排序。

每個 <th> 都有屬性中的類型,如下所示

<table id="grid">
  <thead>
    <tr>
      <th data-type="number">Age</th>
      <th data-type="string">Name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>5</td>
      <td>John</td>
    </tr>
    <tr>
      <td>10</td>
      <td>Ann</td>
    </tr>
    ...
  </tbody>
</table>

在上面的範例中,第一欄是數字,第二欄是字串。排序函式應根據類型處理排序。

僅應支援 "string""number" 類型。

工作範例

附註:表格可以很大,列和欄的數量不限。

為任務開啟沙盒。

重要性:5

為工具提示行為建立 JS 程式碼。

當滑鼠移到具有 data-tooltip 的元素上時,工具提示應出現在其上方,而當滑鼠離開時則隱藏。

註解 HTML 的範例

<button data-tooltip="the tooltip is longer than the element">Short button</button>
<button data-tooltip="HTML<br>tooltip">One more button</button>

應像這樣運作

在此任務中,我們假設所有具有 data-tooltip 的元素內部只有文字。沒有巢狀標籤(目前)。

詳細資料

  • 元素與工具提示之間的距離應為 5px
  • 如果可能,工具提示應相對於元素置中。
  • 工具提示不應超出視窗邊緣。通常它應在元素上方,但如果元素位於頁面頂端且沒有工具提示的空間,則應在元素下方。
  • 工具提示內容在 data-tooltip 屬性中給出。它可以是任意 HTML。

您需要兩個事件

  • 當指標移到元素上方時,mouseover 會觸發。
  • 當指標離開元素時,mouseout 會觸發。

請使用事件委派:在 document 上設定兩個處理常式,以追蹤具有 data-tooltip 的元素的所有「移入」和「移出」,並從那裡管理工具提示。

在實作行為後,即使不熟悉 JavaScript 的人也可以新增註解元素。

附註:一次只能顯示一個工具提示。

為任務開啟沙盒。

教學課程地圖

留言

留言前請閱讀此內容…
  • 如果您有改善建議,請 提交 GitHub 問題 或提交拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入少數幾個字元的程式碼,請使用 <code> 標籤,對於多行程式碼,請將其包覆在 <pre> 標籤中,對於超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)