After I implement this using pure CSS. I started using JavaScript and style classes to round out the functionality.
Then, I have some ideas, I want to use Delegated Events (event delegation) but I don’t want to have any dependencies, plug in any libraries, including jQuery. I need to implement event delegation myself.
Let’s first take a look at what event delegation is? How do they work and how to implement this mechanism.
Okay, what problem does it solve?
Let’s look at a simple example first.
Suppose we have a set of buttons. I click one button at a time, and then I want the clicked state to be set to "active". Cancel active when clicked again.
We can then write 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 can handle the above logic with some standard Javascript events:
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"); }); }
It looks good, but it doesn’t actually work like you expect.
The Trap of Closures
If you have some experience in JavaScript development, this problem will be obvious.
For the uninitiated, the button variable is closed, and the corresponding button will be found every time... But in fact, there is only one button here; it will be reassigned every time it loops.
The first loop it points to the first button, then the second one. But when you click, the button variable always points to the last button element. This is the problem.
What we need is a stable scope; let’s refactor it.
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])); }
Note* The structure of the above code is a bit complicated. You can also simply use a closure to close and save the current button variable, as shown below:
var buttons = document.querySelectorAll(".toolbar .btn"); for(var i = 0; i < buttons.length; i++) { (function(button) { button.addEventListener("click", function() { if(!button.classList.contains("active")) button.classList.add("active"); else button.classList.remove("active"); }); })(buttons[i]) }
Now it can work normally. Pointing to the correct button is always
So what’s the problem with this solution?
This solution looks okay, but we can indeed do better.
First we created too many processing functions. An event listener and a callback handler are bound to each matching .toolbar button. If there are only three buttons this resource allocation can be ignored.
However, what if we have 1,000?
<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 crash either, but it’s not the best solution. We allocate a lot of unnecessary functions. Let's refactor it to attach only once, binding only one function, to handle potentially thousands of calls.
Rather than using a closed button variable to store the object we clicked at that time, we can use the event object to get the object we clicked at that time.
The event object has some metadata. In the case of multiple bindings, we can use currentTarget to obtain the currently bound object. The code in the above example can be changed to:
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); }
Not bad! But this just simplifies a single function and makes it more readable, but it is still bound multiple times.
However, we can do better.
Let us assume that we dynamically add some buttons to this list. Then we'll also add and remove event bindings for these dynamic elements. Then we have to persist the variables used by these processing functions and the current context, which sounds unreliable.
Maybe there are other ways.
Let’s first fully understand how events work and how they are delivered in the DOM.
How events work
When the user clicks on an element, an event is generated to notify the user of the current behavior. There are three stages when events are dispatched:
Capturing stage: Capturing
Triggering stage: Target
Bubbling stage: Bubbling
This event starts from before the document and then goes all the way down to find the object clicked by the current event. When the event reaches the clicked object, it will return along the original path (bubbling process) until it exits the entire DOM tree.
Here is an 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>
When you click Button A, the path of the event will be as follows:
START
| #document
| |
| LI#li_1 /
| BUTTON /
END
Note, this means you can The event generated by your click is captured on the path of the event. We are very sure that this event will definitely pass through their parent element ul element. We can bind our event processing to the parent element and simplify our solution. This is called event delegation and proxy (Delegated Events).
注* 其实Flash/Silverlight/WPF开发的事件机制是非常近似的,这里有一张他们的事件流程图。 除了Silverlight 3使用了旧版IE的仅有冒泡阶段的事件模型外,基本上也都有这三个阶段。(旧版IE和SL3的事件处理只有一个从触发对象冒泡到根对象的过程,可能是为了简化事件的处理机制。)
事件委托代理
委托(代理)事件是那些被绑定到父级元素的事件,但是只有当满足一定匹配条件时才会被挪。
让我们看一个具体的例子,我们看看上文的那个工具栏的例子:
<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元素,让我们将事件处理放到这里试试。我们需要稍微调整一下:
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"); });
这样我们清理了大量的代码,再也没有循环了。注意我们使用了e.target代替了之前的e.currentTarget。这是因为我们在一个不同的层次上面进行了事件侦听。
e.target 是当前触发事件的对象,即用户真正单击到的对象。
e.currentTarget 是当前处理事件的对象,即事件绑定的对象。
在我们的例子中e.currentTarget就是UL.toolbar。
注* 其实不止事件机制,在整个UI构架上FLEX(不是Flash) /Silverlight /WPF /Android的实现跟WEB也非常相似,都使用XML(HTML)实现模板及元素结构组织,Style(CSS)实现显示样式及UI,脚本(AS3,C#,Java,JS)实现控制。不过Web相对其他平台更加开放,不过历史遗留问题也更多。但是几乎所有的平台都支持Web标准,都内嵌有类似WebView这样的内嵌Web渲染机制,相对各大平台复杂的前端UI框架和学习曲线来说,使用Web技术实现Native APP的前端UI是非常低成本的一项选择。