利用原生 Javascript 實現 Delegated Event

想要實現相似於 jQuery 中相似於 .on() 中的 Delegated Event,卻又不想用 jQuery 怎麼破?javascript

先看問題

舉個例子說明一下,有一組按鈕,每當點擊其中一個按鈕,就把這個按鈕的狀態變爲 "active",再點一下就取消 "active" 狀態,代碼以下:html

<ul class="toolbar">
  <li><button class="btn">Pencil</button></li>
  <li><button class="btn">Pen</button></li>
  <li><button class="btn">Eraser</button></li>
</ul>

用最普通的 js 能夠這樣處理:java

var buttons = document.querySelectorAll(".toolbar .btn");

for(var i = 0; i < buttons.length; i++) {
  var button = buttons[i];
  button.addEventListener("click", function() {
    if(!button.classList.contains("active"))
      button.classList.add("active");
    else
      button.classList.remove("active");
  });
}

不過並無達到預期的效果。閉包

閉包惹的禍

有經驗的讀者可能已經看出不對勁的地方了。那是由於處理點擊事件的 handler 函數造成獨立的做用域,是其中的 button 會嘗試去更上級的做用域去尋找。
不過真正當你去點擊按鈕的時候,循環已經完成,button 就會一直指向最後一個按鈕,因此效果就是無論點擊哪一個按鈕都是最後一個按鈕的狀態在變化。app

把代碼改善一下:ide

var buttons = document.querySelectorAll(".toolbar button");
var createToolbarButtonHandler = function(button) {
  return function() {
    if(!button.classList.contains("active"))
      button.classList.add("active");
    else
      button.classList.remove("active");
  };
};

for(var i = 0; i < buttons.length; i++) {
  button.addEventListener("click", createToolBarButtonHandler(buttons[i]));
}

好了,如今就知足要求了。函數

不過。。。

雖然能夠勉強使用,但還能夠作地更好一些。oop

首先上面的代碼會產生許多 handler,在只有三個按鈕的時候仍是能夠接受的。this

不過當有上千個按鈕須要監聽點擊事件的狀況:spa

<ul class="toolbar">
  <li><button id="button_0001">Foo</button></li>
  <li><button id="button_0002">Bar</button></li>
  // ... 997 more elements ...
  <li><button id="button_1000">baz</button></li>
</ul>

就沒那麼輕鬆了,雖然說不會崩潰,但這種方式很是不理想。上面的實現方式是綁定了好多不一樣的卻功能類似的函數,其實根本不須要這樣。只須要綁定一個共享的函數就夠了。

改動很簡單,可使用對應的事件對象做爲 handler 的參數,就能夠經過event.currentTarget很方便地找到對應點擊的按鈕了。

譯者注:這裏的 event.currentTarget 也就至關於 handler 中的 this

var buttons = document.querySelectorAll(".toolbar button");

var toolbarButtonHandler = function(e) {
  var button = e.currentTarget;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
};

for(var i = 0; i < buttons.length; i++) {
  button.addEventListener("click", toolbarButtonHandler);
}

到此咱們的確實現了綁定同一個 handler,並且增長了代碼的可讀性。

不過還能夠作的更好。

假設這樣一種場景,按鈕組中會動態的添加新的按鈕進來,這樣就還得在新添加的按鈕上綁定監聽處理。這就有點麻煩了。

不如換一種方法。

先回想一下 DOM 中 event 的工做原理。

DOM Event 的工做原理簡析

當點擊一個元素,會產生一個點擊事件,這個事件分爲三個階段。

  • Capturing 捕獲階段
  • Target 目標階段
  • Bubbling 冒泡階段

NOTE: Not all events bubble/capture, instead they are dispatched directly on the target, but most do.
The event starts outside the document and then descends through the DOM hierarchy to the target of the event. Once the event reaches it's target, it then turns around and heads back out the same way, until it exits the DOM.
注:雖然並非全部事件的都有 冒泡/捕獲 階段,但絕大部分都有。捕獲階段是從最外層的 document 開始,穿過目標元素的祖先元素,到達目標元素,而後再原路冒泡回到 document。

從一段 HTML 代碼的例子來看:

<html>
<body>
  <ul>
    <li id="li_1"><button id="button_1">Button A</button></li>
    <li id="li_2"><button id="button_2">Button B</button></li>
    <li id="li_3"><button id="button_3">Button C</button></li>
  </ul>
</body>
</html>

若是點擊 Button A 按鈕,事件的過程是這樣的:

START
| #document  \
| HTML        |
| BODY         } CAPTURE PHASE
| UL          |
| LI#li_1    /
| BUTTON     <-- TARGET PHASE
| LI#li_1    \
| UL          |
| BODY         } BUBBLING PHASE 
| HTML        |
v #document  /
END

咱們能夠注意到在事件的冒泡階段,按鈕的祖先元素 ul 也能夠收到點擊事件。咱們能夠利用這個現象和已知元素的層級簡化代碼,實現 Delegated Events。

Delegated Events

Delegated Events 是把事件處理綁定在真正須要被綁定元素的祖先元素上,而後經過必定的條件篩選出真正須要被綁定的元素。

仍是最初的代碼:

<ul class="toolbar">
  <li><button class="btn">Pencil</button></li>
  <li><button class="btn">Pen</button></li>
  <li><button class="btn">Eraser</button></li>
</ul>

既然每次事件冒泡的階段 ul.toolbar 也能夠收到點擊事件,咱們就把事件綁定在它上面。修改對應的 js 代碼:

var toolbar = document.querySelectorAll(".toolbar");
toolbar.addEventListener("click", function(e) {
  var button = e.target;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
});

That cleaned up a lot of code, and we have no more loops! Notice that we use e.target instead of e.currentTarget as we did before. That is because we are listening for the event at a different level.
去掉了 for 循環使代碼看起來清爽多了。注意此次用的是 e.target 而非 e.currentTarget

  • e.target 是事件的目標元素,也就是例子的 button.btn
  • e.currentTarget 是被綁定事件處理的元素,也就是例子中的 ul.toolbar

More Robust Delegated Events

如今已經能夠處理全部 ul.toolbar 後代元素的點擊事件,不過這樣有些太簡單了,咱們須要過濾掉不能被點擊的後代元素:

<ul class="toolbar">
  <li><button class="btn"><i class="fa fa-pencil"></i> Pencil</button></li>
  <li><button class="btn"><i class="fa fa-paint-brush"></i> Pen</button></li>
  <li class="separator"></li>
  <li><button class="btn"><i class="fa fa-eraser"></i> Eraser</button></li>
</ul>

咱們並不須要處理對 li.separator 的點擊事件,那就加一個過濾輔助函數:

var delegate = function(criteria, listener) {
  return function(e) {
    var el = e.target;
    do {
      if (!criteria(el)) continue;
      e.delegateTarget = el;
      listener.apply(this, arguments);
      return;
    } while( (el = el.parentNode) );
  };
};

這個過濾輔助函數的做用,一是判斷 e.target 和它的全部祖先元素是否知足過濾條件。若是知足就在事件對象上增長一個 delegateTarget 屬性,用於後面使用,而後調用事件的處理函數。若是一路檢查全部祖先元素,都不符合條件則不觸發處理函數。

具體使用:

var toolbar = document.querySelector(".toolbar");
var buttonsFilter = function(elem) { return elem.classList && elem.classList.contains("btn"); };
var buttonHandler = function(e) {
  var button = e.delegateTarget;
  if(!button.classList.contains("active"))
    button.classList.add("active");
  else
    button.classList.remove("active");
};
toolbar.addEventListener("click", delegate(buttonsFilter, buttonHandler));

沒錯!就是這個意思。只須要在一個元素上綁定一個 handler,就夠了。而且也不須要擔憂動態增長的元素。這就是所謂的 Delegated Events。

封裝

上面已經實現了在不使用 jQuery 的狀況下實現 Delegated Events。

還能夠把代碼進一步封裝一下:

  • Create helper functions to handle criteria matching in a unified functional way. Something like:
var criteria = {
  isElement: function(e) { return e instanceof HTMLElement; },
  hasClass: function(cls) {
    return function(e) {
      return criteria.isElement(e) && e.classList.contains(cls);
    }
  }
  // More criteria matchers
};
  • A partial application helper would also be nice:
var partialDelgate = function(criteria) {
  return function(handler) { 
    return delgate(criteria, handler);
  }
};

原文連接

相關文章
相關標籤/搜索