利用原生 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);
  }
};

原文连接

相关文章
相关标签/搜索