笔记内容其实是原文: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));
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)