想要实现类似于 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.querySelectorAll(".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);
}
};

原文链接

利用原生 Javascript 实现 Delegated Event相关推荐

  1. vue利用原生javascript 将数组转换成以逗号(或任意符号)隔开的字符串

    前言:有时候页面中用到表格,表格中的某个字段后端接口传的是数组,如果直接显示很不美观. 原先效果图: 目的效果图: 代码: let times= [] listData.forEach(functio ...

  2. javascript写css样式,原生javascript实现读写CSS样式的方法详解

    原生javascript实现读写CSS样式的方法详解 发布于 2017-05-24 15:05:31 | 120 次阅读 | 评论: 0 | 来源: 网友投递 JavaScript客户端脚本语言Jav ...

  3. 原生JavaScript抒写——贪吃蛇小游戏

    原生JavaScript抒写--贪吃蛇小游戏 文章目录 原生JavaScript抒写--贪吃蛇小游戏 前言 一.需求分析 二.效果展示 三.具体逻辑代码分析 1.首先创建一个html文件,然后我们利用 ...

  4. 使用原生javascript实现ajax提交form表单

    使用原生javascript实现ajax提交form表单 ============================ 1 准备表单        首先我们需要编写一个html代码,这里我是采用nodej ...

  5. 分享10个原生JavaScript技巧

    首先在这里要非常感谢无私分享作品的网友们,这些代码片段主要由网友们平时分享的作品代码里面和经常去逛网站然后查看源文件收集到的.把平时网站上常用的一些实用功能代码片段通通收集起来,方便网友们学习使用,利 ...

  6. html 监听input输入框的值,利用原生JS实时监听input框输入值

    利用原生JS实时监听input框输入值 原生JS中可以使用oninput,onpropertychange,onchange oninput,onpropertychange,onchange的用法 ...

  7. mysql插入ㄖ_原生JavaScript代码100个实例

    1.原生JavaScript实现字符串长度截取 function cutstr(str, len) { var temp; var icount = 0; var patrn = /[^\x00-\x ...

  8. 加入收藏代码_100个原生JavaScript代码片段知识点详细汇总【实践】

    作者:小棋子js 转发链接:https://www.jianshu.com/p/b5171efa340f JavaScript 是目前最流行的编程语言之一,正如大多数人所说:"如果你想学一门 ...

  9. 为什么说要学习全新的原生 JavaScript?

    JavaScript 是前端开发工程师最重要的技能,没有之一.在 Vue.js.React.js.Koa.Echarts 等框架风靡一时的背景下,原生的 JavaScript 就可以被抛弃了吗?答案是 ...

最新文章

  1. Windows7/10上快速搭建Tesseract-OCR开发环境操作步骤
  2. 它是最神秘的黑客组织:来自战斗民族 专黑美国
  3. 内核程序员的职位面试技巧
  4. Python基础教程笔记——条件,循环和其他语句
  5. VxWorks任务调度
  6. java代码怎样整体左移_java 多行代码左移
  7. springboot系列六、springboot配置错误页面及全局异常
  8. Android Track的play流程(三十二)
  9. 一步一步教你用 Vue.js + Vuex 制作专门收藏微信公众号的 app
  10. Java 原生 JAXB 解析 XML 深入剖析
  11. TwinCAT 3 xml存储配置文件程序
  12. xp下的资源管理器界面上的前进后退等图标保持在系统哪里?shell32.dll里没有。
  13. 2022,一名85后程序猿之感慨,加油
  14. 《互联网时代》第二集·浪潮
  15. web开发前台,懒人建站资源库
  16. JAVA编写Word
  17. UG数控编程3种螺旋刀路,可用于各种2d和3d加工过程
  18. 中文自然语言预处理总结
  19. 石墨烯 silvaco_华为官方证实,网传石墨烯电池为谣言
  20. json数据转换base64方法

热门文章

  1. 度量学习 (Metric Learning)(一)
  2. 矩阵陈列:平移、缩放、旋转、镜像、矩阵相乘、变化的实现_NXopen-UG二次开发_新浪博客
  3. Android 简介
  4. php设置timezone,php设置 timeZone方法
  5. 微信小程序 解决时间只显示年月日的问题(手机端显示NAN-NAN-NAN)
  6. 蔡徐坤成为Jo Malone London祖玛珑全新品牌代言人
  7. 阿里高级面试题 2019
  8. 【第七周:Python(四)】7周成为数据分析师
  9. 当php邂逅windows通用上传缺陷
  10. 气瓶充装解决方案 压缩气体充装 工业气体快速连接器 格雷希尔G50系列