笔记:《Understanding Delegated JavaScript Events》


笔记内容其实是原文:Understanding Delegated JavaScript 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>
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');
    }
  });
}

这样的坏处是,当循环完毕之后,变量 button 会指向页面中的最后一个 button 元素,所以结果是所有的切换 class 的变化都只会显示在这最后一个 button 元素上。

See the Pen JavaScript Delegate 1 by Lien (@movii) on CodePen.


优化 JavaScript,需要让每一个元素都存在一个稳定的域里。 完善后的 JavaScript:

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

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

See the Pen JavaScript Delegate 2 by Lien (@movii) on CodePen.


So, What’s the problem ?

在需要绑定事件监听的元素还少的时候,这样做没什么问题;但是如果像以下这样,需要绑定的元素非常多的话,这样的事件绑定方式就不一定是最理想的:

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

所以,接续优化:接下来我们将利用函数中的 event 对象,将 event 对象的 currentTarget 属性作为一个参数,告诉我函数我们具体的 click 事件具体是 click 到了哪一个元素上面。

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

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

  console.log(button.innerHTML);
};

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

DEMO:

See the Pen JavaScript Delegate 3 by Lien (@movii) on CodePen.


Okay, how do (most) events work?

大部分的 Event 事件发生的都存在以下三个阶段:

  • Capturing
  • Target
  • Bubbling

NOTE: Not all events bubble/capture, instead they are dispatched directly on the target, but most do.

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

按以上的 HTML 结构,如果点击事件发生在 Button A 上,那整个的 Event 的过程会如下图所示一样发生,捕捉-目标-冒泡三个过程。

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

注意到整个点击事件发生的路径,对于 DOM 中任何一个发生点击事件的 button ,我们都可以飞航确定:在事件发生后,事件的冒泡过程会途径 ul 元素。


代理事件是一种将事件监听器交给目标元素的父级元素的方法,不过要当符合一定的条件才回触发。

结构:

<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 元素上,那我们就直接把事件监听器挂到 UL.toolbar 上:

var buttons = document.querySelectorAll('.toolbar');

var createToolbarButtonHandler = function (e){
  var button = e.target;
  if (!button.classList.contains('active')){
    button.classList.add('active');
  } else {
    button.classList.remove('active');
  }
};

这里值得注意的是,我们将原来的 e.currentTarget 换成了 e.target

  • e.target is actual target of the event. Where the event is trying to get to, or where it came from, in the DOM.
  • e.currentTarget is the current element that is handling the event.

在我们的例子里:currentTarget 始终会是 UL.toolbar


More Robust Delegated Events

以上完成的事件代理方法,在匹配元素的地方太简单,如果我们的 DOM 结构稍微复杂一点,比如说包含了一下不能点击的元素:icon 或者 item

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

这个时候,如果点击事件发生在 <i class="fa fa-pencil"></i> 或者 <li class="separator"></li> 上,那么也会在这些元素的 class 发生添加、删除 active class 的操作。


delegate() 方法:

function delegate (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)
  }
}
var toolbar = document.querySelector(".toolbar");

var buttonFilter = 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');
  }
};

function delegate (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)
  }
}

toolbar.addEventListener('click', delegate(buttonFilter, buttonHandler));

感谢阅读

你们好, 2018 年初把小站从 Jekyll 迁移到 Hugo 的过程中,删除了评论区放的 Disqus 插件,考虑有二:首先无论评论、还是对笔记内容的进一步讨论,读者们更喜欢通过邮件、或者 Twitter 私信的方式来沟通;其次一年多以来 Disqus 后台能看到几乎都是垃圾留言(spam),所以这里直接贴一下邮件、以及 Twitter 账户 地址。

技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。

BEST
Lien(A.K.A 胡椒)
本站总访问量 本站总访客量 本文总阅读量