理解Javascript中的事件绑定与事件委托
Opened this issue · 0 comments
最近在深入实践 js 中,遇到了一些问题,比如我需要为动态创建的 DOM 元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下 js 中的事件绑定与事件委托。
事件绑定
最直接的事件绑定:HTML 事件处理程序
如下示例代码,通过节点属性显式声明,直接在 HTML 中,显式地为按钮绑定了 click 事件,当该按钮有用户点击行为时,便会触发 myClickFunc
方法。
/* html */
<button id='btn' onclick='myClickFunc()'>
ClickMe
</button>;
/* js */
// 事件处理程序
var myClickFunc = function(evt) {
// TODO..
};
// 移除事件处理程序
myClickFunc = function() {};
显而易见,这种绑定方式非常不友好,HTML 代码和 JS 代码严重耦合在一起,比如当要修改一个函数名时候,就要修改两次,
DOM 0 级事件处理程序
通过 DOM 操作动态绑定事件,是一种比较传统的方式,把一个函数赋值给事件处理程序。这种方式也是应用较多的方式,比较简单。看下面例子:
/* html */
<button id='btn'>ClickMe</button>;
/* js */
// 事件处理程序
var myClickFunc = function(evt) {
// TODO ...
};
// 直接给DOM节点的 onclick 方法赋值,注意这里接收的是一个function
document.getElementById('btn').onclick = myClickFunc;
// 移除事件处理程序
document.getElementById('btn').onclick = null;
DOM 2 级事件处理程序
通过事件监听的方式绑定事件,DOM2 级事件定义了两个方法,用于处理指定和删除事件处理程序的操作。
// event: 事件名称
// function: 事件函数
// boolean: false | true, true 为事件捕获, false 为事件冒泡(默认);
Ele.addEventListener(event,function[,boolean]); // 添加句柄
ELe.removeEventListener(event,function[,boolean]); // 移除句柄
看个例子:
/* html */
<button id="btn">ClickMe</button>
/* js */
// 通过DOM操作进行动态绑定:
// 获取btnHello节点
var oBtn = document.getElementById('btn');
// 增加第一个 click 事件监听处理程序
oBtn.addEventListener('click',function(evt){
// TODO sth 1...
});
// 增加第二个 click 事件监听处理程序
oBtn.addEventListener('click',function(evt){
// TODO sth 2...
});
// ps:通过这种形式,可以给btn按钮绑定任意多个click监听;注意,执行顺序与添加顺序相关。
// 移除事件处理程序
oBtn.removeEventListener('click',function(evt){..});
IE 事件处理程序
DOM 2 级事件处理程序在 IE 是行不通的,IE 有自己的事件处理程序方法:attachEvent()
和detachEvent()
。这两个方法的用法与addEventListener()
是一样的,但是只接收两个参数,一个是事件名称,另一个是事件处理程序的函数。为什么不使用第三个参数的原因呢?因为 IE8 以及更早的浏览器版本只支持事件冒泡。看个例子:
/* html */
<button id='btn'>ClickMe</button>;
/* js */
var oBtn = document.getElementById('btn');
// 事件处理函数
function evtFn() {
console.log(this);
}
// 添加句柄
oBtn.attachEvent('onclick', evtFn);
// 移除句柄
oBtn.detachEvent('onclick', evtFn);
简易的跨浏览器解决方法
如果我们既要支持 IE 的事件处理方法,又要支持 DOM 2 级事件,那么就要封装一个跨浏览器的事件处理函数,如果支持 DOM 2 级事件,就用addEventListener
,否则就用attachEvent
。例子如下:
//跨浏览器事件处理程序
var eventUtil = {
// 添加句柄
addHandler: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent('on' + type, handler);
} else {
element['on' + type] = handler;
}
},
// 删除句柄
removeHandler: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent('on' + type, handler);
} else {
element['on' + type] = null;
}
},
};
var oBtn = document.getElementById('btn');
function evtFn() {
alert('hello world');
}
eventUtil.addHandler(oBtn, 'click', evtFn);
eventUtil.removeHandler(oBtn, 'click', evtFn);
事件冒泡和事件捕获
在了解事件委托之前,要先了解下事件冒泡和事件捕获。
早期的 web 开发,浏览器厂商很难回答一个哲学上的问题:当你在页面上的一个区域点击时,你真正感兴趣的是哪个元素。这个问题带来了交互的定义。在一个元素的界限内点击,显得有点含糊。毕竟,在一个元素上的点击同时也发生在另一个元素的界限内。例如单击一个按钮。你实际上点击了按钮区域、body 元素的区域以及 html 元素的区域。
伴随着这个问题,两种主流的浏览器 Netscape 和 IE 有不同的解决方案。Netscape 定义了一种叫做事件捕获的处理方法,事件首先发生在 DOM 树的最高层对象(document)然后往最深层的元素传播。在图例中,事件捕获首先发生在 document 上,然后是 html 元素,body 元素,最后是 button 元素。
IE 的处理方法正好相反。他们定义了一种叫事件冒泡的方法。事件冒泡认为事件促发的最深层元素首先接收事件。然后是它的父元素,依次向上,知道 document 对象最终接收到事件。尽管相对于 html 元素来说,document 没有独立的视觉表现,他仍然是 html 元素的父元素并且事件能冒泡到 document 元素。所以图例中噢噢那个 button 元素先接收事件,然后是 body、html 最后是 document。如下图:
事件冒泡
简单点说,事件冒泡就是事件触发时,会从目标 DOM 元素向上传播,直到文档根节点,一般情况下,会是如下形式传播:
targetDOM → parentNode → ... → body → document → window
如果希望一次事件触发能在整个 DOM 树上都得到响应,那么就需要用到事件冒泡的机制。看下面示例:
/* html */
<button id='btn'>ClickMe</button>;
/* js */
// 给按钮增加click监听
document.getElementById('btn').addEventListener(
'click',
function(evt) {
alert('button clicked');
},
false
);
// 给body增加click监听
document.body.addEventListener(
'click',
function(evt) {
alert('body clicked');
},
false
);
在这种情况下,点击按钮“ClickMe”后,其自身的 click 事件会被触发,同时,该事件将会继续向上传播, 所有的祖先节点都将得到事件的触发命令,并立即触发自己的 click 事件;所以如上代码,将会连续弹出两个 alert.
在有些时候,我们想让事件独立触发,所以我们必须阻止冒泡,用 event 的stopPropagation()
方法。
<button id='btn'>ClickMe</button>;
/* js */
// 给按钮增加click监听
document.getElementById('btn').addEventListener(
'click',
function(evt) {
alert('button clicked');
evt.stopPropagation(); //阻止事件冒泡
},
false
);
// 给body增加click监听
document.body.addEventListener(
'click',
function(evt) {
alert('body clicked');
},
false
);
此时,点击按钮后,只会触发按钮本身的 click 事件,得到一个 alert 效果;该按钮的点击事件,不会向上传播,body 节点就接收不到此次事件命令。
需要注意的是:
- 不是所有的事件都能冒泡,如:
blur
、focus
、load
、unload
事件都不能 - 不同的浏览器,阻止冒泡的方式也不一样,在 w3c 标准中,通过
event.stopPropagation()
完成, 在 IE 中则是通过自身的event.cancelBubble=true
来完成。
事件委托
事件委托看起来挺难理解,但是举个生活的例子。比如,有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台 MM 代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台 MM 收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台 MM 也会在收到寄给新员工的快递后核实并代为签收。举个例子
HTML 结构:
<ul id="ul-item">
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
</ul>
如果我们要点击 li 标签,弹出里面的内容,我们就需要为每个 li 标签绑定事件。
(function() {
var oUlItem = document.getElementById('ul-item');
var oLi = oUlItem.getElementsByTagName('li');
for (var i = 0, l = oLi.length; i < l; i++) {
oLi[i].addEventListener('click', show);
}
function show(e) {
e = e || window.event;
alert(e.target.innerHTML);
}
})();
虽然这样子能够实现我们想要的功能,但是如果这个 ul 中的 li 子元素频繁的添加或删除,我们就需要在每次添加 li 的时候为它绑定事件。这就添加了复杂度,并且造成内存开销较大。
更简单的方法是利用事件委托,当事件被抛到更上层的父节点的时候,通过检查事件的目标对象(target)来判断并获取事件源 li。
(function() {
var oUlItem = document.getElementById('ul-item');
oUlItem.addEventListener('click', show);
function show(e) {
e = e || window.event;
var src = e.target;
if (src && src.nodeName.toLowerCase() === 'li') {
alert(src.innerHTML);
}
}
})();
这里我们为父节点 UL 添加了点击事件,当点击子节点 li 标签的时候,点击事件会冒泡到父节点。父节点捕获到事件之后,通过判断e.target.nodeName
来判断是否为我们需要处理的节点,并且通过e.target
拿到了被点击的 li 节点。从而可以获取到相应的信息,并做处理。
优点:
通过上面的介绍,大家应该能够体会到使用事件委托对于 web 应用程序带来的几个优点:
-
管理的函数变少了。不需要为每个元素都添加监听函数。对于同一个父节点下面类似的子元素,可以通过委托给父元素的监听函数来处理事件。
-
可以方便地动态添加和修改元素,不需要因为元素的改动而修改事件绑定。
-
JavaScript 和 DOM 节点之间的关联变少了,这样也就减少了因循环引用而带来的内存泄漏发生的概率。