资讯专栏INFORMATION COLUMN

利用原生 Javascript 实现 Delegated Event

jiekechoo / 944人阅读

摘要:我们可以利用这个现象和已知元素的层级简化代码,实现。注意这次用的是而非。如果一路检查所有祖先元素,都不符合条件则不触发处理函数。封装上面已经实现了在不使用的情况下实现。

想要实现类似于 jQuery 中类似于 .on() 中的 Delegated Event,却又不想用 jQuery 怎么破?

先看问题

举个例子说明一下,有一组按钮,每当点击其中一个按钮,就把这个按钮的状态变为 "active",再点一下就取消 "active" 状态,代码如下:

用最普通的 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,在只有三个按钮的时候还是可以接受的。

不过当有上千个按钮需要监听点击事件的情况:

  • // ... 997 more elements ...

就没那么轻松了,虽说不会崩溃,但这种方式非常不理想。上面的实现方式是绑定了好多不同的却功能相似的函数,其实根本不需要这样。只需要绑定一个共享的函数就够了。

改动很简单,可以使用对应的事件对象作为 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 代码的例子来看:



  

如果点击 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.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 后代元素的点击事件,不过这样有些太简单了,我们需要过滤掉不能被点击的后代元素:

我们并不需要处理对 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);
  }
};

原文链接

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85367.html

相关文章

  • 前端面试(知识点)整理(一)

    摘要:接受个参数事件类型,是否冒泡,是否阻止浏览器的默认行为触发上绑定的自定义事件触发元素上绑定事件事件的委托代理的原理以及优缺点。委托代理事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。 一、页面布局 1.问题:假设高度已知,请写出三栏布局 ,其中左栏、右栏宽度各为300px,中间自适应。 解决方案一:使用浮动布局` Document ...

    zhichangterry 评论0 收藏0
  • 前端面试(知识点)整理(一)

    摘要:接受个参数事件类型,是否冒泡,是否阻止浏览器的默认行为触发上绑定的自定义事件触发元素上绑定事件事件的委托代理的原理以及优缺点。委托代理事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。 一、页面布局 1.问题:假设高度已知,请写出三栏布局 ,其中左栏、右栏宽度各为300px,中间自适应。 解决方案一:使用浮动布局` Document ...

    Songlcy 评论0 收藏0
  • 前端面试(知识点)整理(一)

    摘要:接受个参数事件类型,是否冒泡,是否阻止浏览器的默认行为触发上绑定的自定义事件触发元素上绑定事件事件的委托代理的原理以及优缺点。委托代理事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。 一、页面布局 1.问题:假设高度已知,请写出三栏布局 ,其中左栏、右栏宽度各为300px,中间自适应。 解决方案一:使用浮动布局` Document ...

    ThreeWords 评论0 收藏0
  • 手把手教你用原生JavaScript造轮子(1)——分页器(最后更新:Vue插件版本,本篇Over!

    摘要:使用构造函数那么有没有一种办法,可以不写函数名,直接声明一个函数并自动调用它呢答案肯定的,那就是使用自执行函数。 日常工作中经常会发现有大量业务逻辑是重复的,而用别人的插件也不能完美解决一些定制化的需求,所以我决定把一些常用的组件抽离、封装出来,形成一套自己的插件库。同时,我将用这个教程系列记录下每一个插件的开发过程,手把手教你如何一步一步去造出一套实用性、可复用性高的轮子。 So, ...

    CHENGKANG 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<