应用原生 Javascript 完成 Delegated Event

想要完成类似于 jQuery 中类似于 .on() 中的 Delegated Event,却又不想用 jQuery 怎样破?

先看题目

举个例子申明一下,有一组按钮,每当点击个中一个按钮,就把这个按钮的状况变成 “active”,再点一下就作废 “active” 状况,代码以下:

<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 能够如许处置惩罚:

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 就会一向指向末了一个按钮,所以结果就是不论点击哪一个按钮都是末了一个按钮的状况在变化。

把代码改良一下:

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

好了,如今就满足要求了。

不过。。。

虽然能够委曲运用,但还能够做地更好一些。

起首上面的代码会发生很多 handler,在只要三个按钮的时刻照样能够接收的。

不过当有上千个按钮须要监听点击事宜的状况:

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

原文链接

    原文作者:2hu10n92hen9
    原文地址: https://segmentfault.com/a/1190000000747945
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞