理解JavaScript中的事件路由冒泡過程及委託代理機制

edithfang發表於2014-11-27
當我用純CSS實現這個以後。我開始用JavaScript和樣式類來完善功能。

然後,我有一些想法,我想使用Delegated Events (事件委託)但是我不想有任何依賴,插入任何庫,包括jQuery。我需要自己實現事件委託了。

我們先來看看事件委託到底是什麼?他們是怎麼工作的,怎麼去實現這種機制。

好,它解決了什麼問題?

我們先看個簡單的例子。

先假設我們有一組按鈕,我一次點選一個按鈕,然後我希望被點中的狀態設為"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>


我可以用一些標準的Javascript事件處理上面的邏輯:

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");
  });
}


看上去不錯,但是它其實不能像你期望的那樣工作。 

閉包的陷阱

如果你有一定的JavaScript開發經驗,這個問題就很明顯了。

對於外行來說button變數是被封閉的,每次都會找到對應的button……但是其實這裡只有一個button;每次迴圈都會被重新分配。

第一個迴圈它指向第一個button,接下來是第二個。但當你點選時button變數永遠只指向最後一個button元素,問題出在這。

我們需要的是一個穩定的作用域;讓我們重構一下。

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++) {
  buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i]));
}


注* 上面這段程式碼結構有點複雜,也可以簡單直接地使用一個閉包,封閉儲存當前的button變數,如下所示:

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

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


現在它能正常工作了。指向永遠是正確的button 

那麼這個方案有什麼問題?

這個方案看上去還可以,然而我們確實可以做得更好。

首先我們建立了太多的處理函式。為每一個匹配的.toolbar button繫結了一個事件偵聽和一個回撥處理。假如只有三個按鈕這種資源分配是可以忽略的。

然而,如果我們有1000個呢?

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


它也不會崩潰,但是這並不是最佳的方案。我們分配了大量不必要的函式。讓我們重構一下,僅附加一次,即僅繫結一個函式(function),去處理這種有可能的數千次呼叫。

相對於封閉button變數去儲存當時我們點選的物件,我們可以使用event物件去獲取當時點選的物件。

event物件有一些後設資料,在多次繫結的種情況下,我們可以使用currentTarget獲取當前繫結的物件,如上例的程式碼就可以改成:

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);
}


不錯!不過這只是簡化了單個函式,讓它得更具可讀性,然而它還是被繫結了多次。

但是,我們還可以做得更好。

讓我們假設一下,我們在這個列表裡動態地新增了一些按鈕。然後我們還要為這些動態元素新增和移除事件繫結。然後我們還要持久化這些處理函式和當前上下文要用到的變數,這事聽上去就不靠譜。

也許還有其他方法。

讓我們先全面理解一下事件的工作原理,以及他們在DOM裡是怎樣傳遞的。

事件的工作原理

當使用者點選一個元素時,一個事件就會被產生去通知使用者當前的行為。事件在分發派遣時會有三個階段:

  • 捕獲階段: Capturing 
  • 觸發階段: Target
  • 冒泡階段: Bubbling


這個事件起始從document之前然後一路向下找到當前事件點選到的物件。當事件達到點選到的物件之後,它會按原路返回(冒泡過程),直到退出整個DOM樹。

這裡是一個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)。

注* 其實Flash/Silverlight/WPF開發的事件機制是非常近似的,這裡有一張他們的事件流程圖。 除了Silverlight 3使用了舊版IE的僅有冒泡階段的事件模型外,基本上也都有這三個階段。(舊版IE和SL3的事件處理只有一個從觸發物件冒泡到根物件的過程,可能是為了簡化事件的處理機制。)


事件委託代理

委託(代理)事件是那些被繫結到父級元素的事件,但是隻有當滿足一定匹配條件時才會被挪。

讓我們看一個具體的例子,我們看看上文的那個工具欄的例子:

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


因為我們知道單擊button元素會冒泡到UL.toolbar元素,讓我們將事件處理放到這裡試試。我們需要稍微調整一下:

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


這樣我們清理了大量的程式碼,再也沒有迴圈了。注意我們使用了e.target代替了之前的e.currentTarget。這是因為我們在一個不同的層次上面進行了事件偵聽。

  • e.target 是當前觸發事件的物件,即使用者真正單擊到的物件。
  • e.currentTarget 是當前處理事件的物件,即事件繫結的物件。


在我們的例子中e.currentTarget就是UL.toolbar。

注* 其實不止事件機制,在整個UI構架上FLEX(不是Flash) /Silverlight /WPF /Android的實現跟WEB也非常相似,都使用XML(HTML)實現模板及元素結構組織,Style(CSS)實現顯示樣式及UI,指令碼(AS3,C#,Java,JS)實現控制。不過Web相對其他平臺更加開放,不過歷史遺留問題也更多。但是幾乎所有的平臺都支援Web標準,都內嵌有類似WebView這樣的內嵌Web渲染機制,相對各大平臺複雜的前端UI框架和學習曲線來說,使用Web技術實現Native APP的前端UI是非常低成本的一項選擇。

原文地址: codepen.io
評論(1)

相關文章