Javascript 事件委托

2023-02-17 10:54 更新

捕獲和冒泡允許我們實現(xiàn)最強大的事件處理模式之一,即 事件委托 模式。

這個想法是,如果我們有許多以類似方式處理的元素,那么就不必為每個元素分配一個處理程序 —— 而是將單個處理程序放在它們的共同祖先上。

在處理程序中,我們獲取 event.target 以查看事件實際發(fā)生的位置并進行處理。

讓我們看一個示例 —— 反映中國古代哲學(xué)的 八卦圖

如下所示:


其 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 個單元格(cell),但可以有 99 個或 9999 個單元格,這都不重要。

我們的任務(wù)是在點擊時高亮顯示被點擊的單元格 <td>。

與其為每個 <td>(可能有很多)分配一個 onclick 處理程序 —— 我們可以在 <table> 元素上設(shè)置一個“捕獲所有”的處理程序。

它將使用 event.target 來獲取點擊的元素并高亮顯示它。

代碼如下:

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // 在哪里點擊的?

  if (target.tagName != 'TD') return; // 不在 TD 上?那么我們就不會在意

  highlight(target); // 高亮顯示它
};

function highlight(td) {
  if (selectedTd) { // 移除現(xiàn)有的高亮顯示,如果有的話
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // 高亮顯示新的 td
}

此代碼不會關(guān)心在表格中有多少個單元格。我們可以隨時動態(tài)添加/移除 <td>,高亮顯示仍然有效。

盡管如此,但還是存在缺陷。

點擊可能不是發(fā)生在 <td> 上,而是發(fā)生在其內(nèi)部。

在我們的例子中,如果我們看一下 HTML 內(nèi)部,我們可以看到 <td> 內(nèi)還有嵌套的標簽,例如 <strong>

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

自然地,如果在該 <strong> 上點擊,那么它將成為 event.target 的值。


在處理程序 table.onclick 中,我們應(yīng)該接受這樣的 event.target,并確定該點擊是否在 <td> 內(nèi)。

下面是改進后的代碼:

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)? 方法返回與 ?selector? 匹配的最近的祖先。在我們的例子中,我們從源元素開始向上尋找 ?<td>?。
  2. 如果 ?event.target? 不在任何 ?<td>? 中,那么調(diào)用將立即返回,因為這里沒有什么事兒可做。
  3. 對于嵌套的表格,?event.target? 可能是一個 ?<td>?,但位于當前表格之外。因此我們需要檢查它是否是 我們的表格中的 ?<td>?。
  4. 如果是的話,就高亮顯示它。

最終,我們得到了一個快速、高效的用于高亮顯示的代碼,該代碼與表格中的 <td> 的數(shù)量無關(guān)。

委托示例:標記中的行為

事件委托還有其他用途。(譯注:本節(jié)標題中的“標記中的行為”即 action in markup)

例如,我們想要編寫一個有“保存”、“加載”和“搜索”等按鈕的菜單。并且,這里有一個具有 saveload 和 search 等方法的對象。如何匹配它們?

第一個想法可能是為每個按鈕分配一個單獨的處理程序。但是有一個更優(yōu)雅的解決方案。我們可以為整個菜單添加一個處理程序,并為具有方法調(diào)用的按鈕添加 data-action 特性(attribute):

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

處理程序讀取特性(attribute)并執(zhí)行該方法。工作示例如下:

<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。這很重要,因為否則內(nèi)部的 this 將引用 DOM 元素(elem),而不是 Menu 對象,那樣的話,this[action] 將不是我們所需要的。

那么,這里的委托給我們帶來了什么好處?

  • 我們不需要編寫代碼來為每個按鈕分配一個處理程序。只需要創(chuàng)建一個方法并將其放入標記(markup)中即可。
  • HTML 結(jié)構(gòu)非常靈活,我們可以隨時添加/移除按鈕。

我們也可以使用 .action-save,.action-load 類,但 data-action 特性(attribute)在語義上更好。我們也可以在 CSS 規(guī)則中使用它。

“行為”模式

我們還可以使用事件委托將“行為(behavior)”以 聲明方式 添加到具有特殊特性(attribute)和類的元素中。

行為模式分為兩個部分:

  1. 我們將自定義特性添加到描述其行為的元素。
  2. 用文檔范圍級的處理程序追蹤事件,如果事件發(fā)生在具有特定特性的元素上 —— 則執(zhí)行行為(action)。

行為:計數(shù)器

例如,這里的特性 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) { // 如果這個特性存在...
      event.target.value++;
    }

  });
</script>

如果我們點擊按鈕 —— 它的值就會增加。但不僅僅是按鈕,一般的方法在這里也很重要。

我們可以根據(jù)需要使用 data-counter 特性,多少都可以。我們可以隨時向 HTML 添加新的特性。使用事件委托,我們屬于對 HTML 進行了“擴展”,添加了描述新行為的特性。

對于文檔級的處理程序 —— 始終使用的是 ?addEventListener?

當我們將事件處理程序分配給 document 對象時,我們應(yīng)該始終使用 addEventListener,而不是 document.on<event>,因為后者會引起沖突:新的處理程序會覆蓋舊的處理程序。

對于實際項目來說。在 document 上有許多由代碼的不同部分設(shè)置的處理程序,這是很正常的。

行為:切換器

再舉一個例子。點擊一個具有 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>

讓我們再次注意我們做了什么。現(xiàn)在,要向元素添加切換功能 —— 無需了解 JavaScript,只需要使用特性 data-toggle-id 即可。

這可能變得非常方便 —— 無需為每個這樣的元素編寫 JavaScript。只需要使用行為。文檔級處理程序使其適用于頁面的任意元素。

我們也可以組合單個元素上的多個行為。

“行為”模式可以替代 JavaScript 的小片段。

總結(jié)

事件委托真的很酷!這是 DOM 事件最有用的模式之一。

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

算法:

  1. 在容器(container)上放一個處理程序。
  2. 在處理程序中 —— 檢查源元素 ?event.target?。
  3. 如果事件發(fā)生在我們感興趣的元素內(nèi),那么處理該事件。

好處:

  • 簡化初始化并節(jié)省內(nèi)存:無需添加許多處理程序。
  • 更少的代碼:添加或移除元素時,無需添加/移除處理程序。
  • DOM 修改 :我們可以使用 ?innerHTML? 等,來批量添加/移除元素。

事件委托也有其局限性:

  • 首先,事件必須冒泡。而有些事件不會冒泡。此外,低級別的處理程序不應(yīng)該使用 ?event.stopPropagation()?。
  • 其次,委托可能會增加 CPU 負載,因為容器級別的處理程序會對容器中任意位置的事件做出反應(yīng),而不管我們是否對該事件感興趣。但是,通常負載可以忽略不計,所以我們不考慮它。

任務(wù)


使用委托隱藏消息

重要程度: 5

有一個帶有移除按鈕 ?[x]? 的消息列表。讓按鈕可以工作。

P.S. 在容器上應(yīng)該只有一個事件監(jiān)聽器,請使用事件委托。

打開一個任務(wù)沙箱。


解決方案

使用沙箱打開解決方案。


樹形菜單

重要程度: 5

創(chuàng)建一個點擊可以顯示/隱藏子節(jié)點的樹形菜單:

要求:

  • 只能有一個事件處理程序(使用委托)。
  • 對節(jié)點標題以外(在空白處)的點擊不會做任何處理。

打開一個任務(wù)沙箱。


解決方案

解決方案分為兩個部分。

  1. 將每個樹節(jié)點的標題都包裝到 ?<span>? 中。然后我們可以在 ?:hover? 上使用 CSS 樣式,并精確地處理文本上的點擊事件,因為 ?<span>? 的寬度恰好是文本的寬度(與沒有寬度不同)。
  2. 為? tree? 的根節(jié)點設(shè)置一個處理程序,來處理 ?<span>? 標題上的點擊事件。

使用沙箱打開解決方案。


可排序的表格

重要程度: 4

使表格可排序:點擊 <th> 元素,應(yīng)按對應(yīng)的列對表格進行排序。

每個 <th> 的特性(attribute)中都有類型,如下所示:

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

在上面的示例中,第一列為數(shù)字,第二列為字符串。排序函數(shù)應(yīng)根據(jù)類型進行排序。

應(yīng)該只支持 "string" 和 "number" 類型。

P.S. 表格可以更大,有任意數(shù)量的行和列。

打開一個任務(wù)沙箱。


解決方案

使用沙箱打開解決方案。


工具提示行為

重要程度: 5

編寫工具提示(tooltip)行為的 JavaScript 代碼。

當鼠標在帶有 data-tooltip 的元素的上方時,工具提示應(yīng)顯示在其上方,當鼠標移開時,工具提示將隱藏起來。

在此任務(wù)中,我們假設(shè)所有具有 data-tooltip 的元素中都只有文本。尚無嵌套標簽。

詳情:

  • 元素和工具提示之間的距離應(yīng)為 ?5px?。
  • 如果可能,工具提示應(yīng)相對于元素居中。
  • 工具提示不應(yīng)與窗口邊緣交叉。通常,它應(yīng)該在元素的上方,但是如果元素位于頁面頂部,并且沒有工具提示的空間,則應(yīng)該在元素的下方。
  • 工具提示的內(nèi)容在 ?data-tooltip? 屬性中給定。它可以是任意 HTML。

在這里你將需要兩個事件:

  • ?mouseover? 當鼠標指針出現(xiàn)在元素上方時觸發(fā)。
  • ?mouseout? 當鼠標指針離開元素時觸發(fā)。

請使用事件委托:在 document 上設(shè)置兩個處理程序,以跟蹤帶有 data-tooltip 的元素中的所有 “over” 和 “out”,并從那里管理工具提示。

在實現(xiàn)了該行為后,即使不熟悉 JavaScript 的人也可以添加帶注釋的元素。

P.S. 一次只能顯示一個工具提示。

打開一個任務(wù)沙箱。


解決方案

使用沙箱打開解決方案。


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號