Understanding Delegated JavaScript Events
While I ended up using a CSS-only implementation for this pen, I started by writing it mostly using classes and JavaScript.
However, I had a conflict. I wanted to use Delegated Events but I also wanted to minimize the dependancies I wanted to inject. I didn't want to have to import all of jQuery for this little test, just to be able to use delegated events one bit.
Let's take a closer look at what exactly delegated events are, how they work, and various ways to implement them.
Ok, so what's the issue?
Let's look at a simplified example:
Let's say that I had a list of buttons and each time I clicked on one, then I want to mark that button as "active". If I click it again, then deactivate it.
So let's start with some 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>
I could use standard JavaScript event handler by doing something like this:
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");
});
}
And this looks good... but it wont' work... Not the way one might expect it to.
Bitten by closures
For those of you that have been doing functional JavaScript for a while, the problem is pretty obvious.
For the uninitiated, the handler function closes over the button variable. However, there is only one of them; it gets reassigned by each iteration of the loop.
The first time though the loop, it points a the first button. The next time, the second button. And so on. However, by the time that you click one of the elements, the loop has completed and the button variable will point at the last element iterated over. Not good.
What we really need is a stable scope for each function; let's refactor an extract a handler generator to give us a stable scope:
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++) {
buttons[i].addEventListener("click", createToolBarButtonHandler(buttons[i]));
}
Better! And now it actually works. We are using a function to create us a stable scope forbutton, so the button in the handler will always point at the element that we think it will.
So, what the problem?
This seems good and it will work for the most part. However, we can still do better.
First, we are making a lot of handlers. For each element that matches .toolbar buttonwe create a function and attach it as an event listener. With the three buttons we have right now the allocations are negligible.
However, what if we had:
<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>
It won't blow up, but it is far from ideal. We are allocating a bunch of function that we don't have to. Let's try to refactor so that we can share a single function that is attachedmultiple times.
Rather than closing over the button variable to keep track of which button we clicked on, we can use event object that is handed to each event handler as the first argument.
The event object contains some metadata about the event. In this case, we want the thecurrentTarget property of the event to get a reference to the element that was actually clicked on.
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);
}
Great! This not only simplified down to a single function that is added multiple times, also made the code more readable by factoring out our generator function.
But, we can still do better.
Let's say we added some buttons dynamically into the list. Then we would also need to remember to wire up the event listeners directly to those dynamic elements. And we would have to hold onto a reference to that handler and reference from more places. That doesn't sound like fun.
Perhaps there is a different approach.
Let's start by getting a better understanding of how events work and how they move through the DOM.
Okay, how do (most) events work?
When the user clicks on an element, an event gets generated to notify the application of the user's intent. Events get dispatched in three phases:
- 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 though 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.
Here is a full HTML example:
<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>
If the user clicks on Button A, then the event would travel like this like this:
START
| #document \
| HTML |
| BODY } CAPTURE PHASE
| UL |
| LI#li_1 /
| BUTTON <-- TARGET PHASE
| LI#li_1 \
| UL |
| BODY } BUBBLING PHASE
| HTML |
v #document /
END
Notice that you can follow the path the event takes down to the element that gets clicked on. For any button we click on in our DOM, we can be sure that the event will bubble back out through our parent ul element. We can exploit this feature of the event dispatcher, combined with our defined hierarchy to simplify our implementation and implement Delegated Events.
Delegated Events
Delegated events are events that are attached to a parent element, but only get executed when the target of the event matches some criteria.
Let's look at a concrete example and switch back to our toolbar example DOM from before:
<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>
So, since we know that any clicks on the button elements will get bubbled through theUL.toolbar element, let's put the event handler there instead. We'll have to adjust our handler a little bit from before;
var toolbar = document.querySelector(".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.targetinstead of e.currentTarget as we did before. That is because we are listening for the event at a different level.
e.targetis actual target of the event. Where the event is trying to get to, or where it came from, in the DOM.e.currentTargetis the current element that is handling the event.
In our case e.currentTarget will be the UL.toolbar.
More Robust Delegated Events
Right now, we handle any click on any element that bubbles though UL.toolbar, but our matching strategy is a little too simple. What if we had more complicated DOM that included icons and items that were supposed to be non-clickable
<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>
OOPS. Now, when we click on the LI.separator or the icons, we add the active class to that element. That's not cool. We need a way to filter our events so we only react to elements we care about, or if our target element is contained by an element we care about.
Let's make a little helper to handle that:
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) );
};
};
This helper does two things, first it walks though each element and their parents to see if it matches a criteria function. If it does, then it adds a property to the event object calleddelegateTarget, which is the element that matched our filtering criteria. And then invokes the listener. If nothing matches, the no handlers are fired.
We can use it like this:
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));
BOOM! That's what I'm talking about: A single event handler, attached to a single element that does all the work, but only does it on the elements that we care about and will react nicely to elements added or removed from the DOM dynamically.
Wrapping up
We've looked at the basics of how to implement event delegation in pure javascript in order to reduce the number of event handlers we need to generate or attach.
There are a few things I would do, if I were going to abstract this into a library, or use it for production level code:
- 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);
}
};
If you have any suggestions or improvments, drop a comment or send me a message! Happy coding!
http://codepen.io/32bitkid/blog/understanding-delegated-javascript-events
Understanding Delegated JavaScript Events的更多相关文章
- Understanding the JavaScript Engine—— two phase
Understanding the JavaScript Engine — Part 1 I have been a Ruby on Rails developer for the last 2 ...
- Javascript Events
事件通常与函数配合使用,这样就可以通过发生的事件来驱动函数执行. 事件句柄 html4.0的新特性之一是有能力使html事件触发浏览器中的动作action,比如当用户点击某个html元素时启动一段Ja ...
- Javascript Madness: Mouse Events
http://unixpapa.com/js/mouse.html Javascript Madness: Mouse Events Jan WolterAug 12, 2011 Note: I ha ...
- JavaScript Interview Questions: Event Delegation and This
David Posin helps you land that next programming position by understanding important JavaScript fund ...
- 推荐15款制作 SVG 动画的 JavaScript 库
在当今时代,SVG是最流行的和正在被众多的设计人员和开发人员使用,创建支持视网膜和响应式的网页设计.绘制SVG不是一个艰巨的任务,因为大量的 JavaScript 库可与 SVG 图像搭配使用.这些J ...
- 常用的Javascript设计模式
<parctical common lisp>的作者曾说,如果你需要一种模式,那一定是哪里出了问题.他所说的问题是指因为语言的天生缺陷,不得不去寻求和总结一种通用的解决方案. 不管是弱类型 ...
- Javascript——浅谈 Event Flow
1.Javascript Events : Event Bubbling(事件冒泡) 如果事件从最特定的元素开始,则事件流中的一个阶段称为事件冒泡(DOM中可能最深的节点)然后向上流向最不特定的节点( ...
- 集成Javascript Logging on MVC or Core
ASP.NET Core provides us a rich Logging APIs which have a set of logger providers including: Console ...
- 每个JavaScript工程师都应懂的33个概念
摘要: 基础很重要啊! 原文:33 concepts every JavaScript developer should know 译文:每个 JavaScript 工程师都应懂的33个概念 作者:s ...
随机推荐
- rolllup巧用
--构造环境drop table dept purge;drop table emp purge;create table dept as select * from scott.dept;creat ...
- Celery+redis实现异步
目录 Celery+redis实现异步 安装redis 安装celery-with-redis 添加celery相关配置 创建异步运行任务tasks.py 启动 Celery+redis实现异步 安装 ...
- angular中ngOnChanges与组件变化检测的关系
1.ngOnChanges只有在输入值改变的时候才会触发,如果输入值(@Input)是一个对象,改变对象内的属性的话是不会触发ngOnChanges的. 2.组件的变化检测: 2a.changeDet ...
- BZOJ1770:[USACO]lights 燈(高斯消元,DFS)
Description 貝希和她的閨密們在她們的牛棚中玩遊戲.但是天不從人願,突然,牛棚的電源跳閘了,所有的燈都被關閉了.貝希是一個很膽小的女生,在伸手不見拇指的無盡的黑暗中,她感到驚恐,痛苦與絕望. ...
- sendmail启动报错
sendmail启动不了,报错如下: 解决方法: 在/etc/mail/sendmail.cf 配置文件中查找 Dj$w,并在此行下面增加这一行. Dj$w. 在/etc/hosts 增加一行 192 ...
- VS2008 工具栏CMFCToolBar的使用总结(转)
(一)自定义工具栏 自定义工具栏,分两种情况:一是直接添加工具栏,并自己绘制图标:二是,添加工具栏,然后与BMP关联,与VC6.0中的自定义彩色工具栏类似. 1. 自绘工具栏 1)添加Toolbar ...
- JQuery手写一个简单的分页
效果图: 大概思路:使用ul进行初始布局,每一次点击事件改变li里的值.完整的代码在gitup上:https://github.com/anxizhihai/Paging.gitcss部分: html ...
- java基础知识(初学)
(小记) 文本文档方式可以下载notepad 在设置-新建-修改默认语言为java 编码为ANSI! java关键字特点:1.完全小写字母.如:public. java标识符:方法的名称,类的名称,变 ...
- 我的前端工具集(六)Ajax封装token
我的前端工具集(六)Ajax封装token liuyuhang原创,未经允许禁止转载 在单点登陆中,或登陆验证后,不应该每次都验证用户名和密码, 也不应该将用户名密码存入cookie中(虽然很多都 ...
- DB数据源之SpringBoot+MyBatis踏坑过程(四)没有使用连接池的后果
DB数据源之SpringBoot+MyBatis踏坑过程(四)没有使用连接池的后果 liuyuhang原创,未经允许禁止转载 系列目录连接 DB数据源之SpringBoot+Mybatis踏坑过程实 ...