事件代理

本文是对 JavaScript 高级程序设计 一书中相关章节的整理,加上自己的理解


1. 内存和性能问题

事件处理程序为网页提供了非常好的交互能力,可是,在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。主要是以下两个方面的原因:

  1. 每个函数都是对象,而对象是占用内存的;内存中的对象越多,性能就越差。
  2. 所有的事件处理程序都要事先绑定,这就导致了 DOM 访问次数过多,而 DOM 访问是很昂贵的,对性能消耗很大,这样就会延迟整个页面的交互就绪时间。

2. 事件代理

减少事件处理程序数量的一个有效方法就是使用 事件代理
事件代理利用了事件冒泡,在 DOM 树中尽量高的层次上添加一个事件处理程序,就可以管理低层次的节点上某一类型的所有事件。我们可以说高层次的节点代理了低层次子节点的某一类型事件。

以下面的代码为例:

<ul id = "myLinks">
<li id = "goSomewhere">Go somewhere </li>
<li id = "doSomething">Do something </li>
<li id = "saySomething">Say something </li>
</ul>

其中包含了3个被单击后会执行某种操作的列表项。按照传统的做法,我们需要像下面一样为它们分别添加事件处理程序。

// 获取列表项节点
var item1 = getElementById("goSomewhere");
var item2 = getElementById("doSomething");
var item3 = getElementById("saySomething");
// 为每个列表项绑定事件处理程序
EventUtil.addHandler(item1, "click", function(event) {
location.href = "http://changxiupeng.github.io";
});
EventUtil.addHandler(item2, "click", function(event) {
document.title = "I changed the document's title";
});
EventUtil.addHandler(item3, "click", function(event) {
alert("Hi");
});

代码中添加事件处理程序的方法采用的是跨浏览器的解决方案,之前文章 介绍过。

如果在一个复杂的网页中,对所有的可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序,产生数不清的函数对象和 DOM 访问。
此时,可以利用事件代理技术解决这个问题。如下面代码所示:

var list = document.getElementById("myLinks");
EventUtil.addHandler(list, "click", function(event) {
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id) {
case "goSomewhere":
location.href = "https://changxiupeng.github.io";
break;
case "doSomething":
document.title = "I changed the document's title";
break;
case "saySomething":
alert("hi");
break;
}
});

上面的代码只为 <ul> 元素添加了一个点击事件处理程序。
由于所有列表项都是这个元素的子节点,而且它们的事件都会冒泡,所以对子节点的单击事件最终都会冒泡到这个元素上,都可以被这个元素上绑定的句柄处理。
我们知道,事件目标是被单击的列表项,故而可以通过检测 id 属性来决定采取适当的操作。
这段代码只取得了一个 DOM 元素,只添加了一个事件处理程序。

最适合采用事件委托技术的事件包括:click、mousedown、mouseup、keypress、keydown、keyup。
虽然,mouseover 和 mouseup 事件也冒泡,但是要适当地处理它们并不容易,经常需要计算元素的位置。因为,当鼠标进入该元素的一个子节点,或者从一个子节点移到另一个子节点时,都会再次触发 mouseover/mouseout 事件。

优点

  1. 可以大量节省内存占用,减少事件注册
  2. 新增子对象时,无需再次对其进行绑定,对于动态内容尤为合适

封装事件代理

/*
* 传入的参数分别为要绑定事件处理程序的高层元素节点、需要委托事件的
* 子节点的选择器、事件的类型、事件处理程序
*/
function delegateEvent(supNode, selector, type, fn) {
// 采用跨浏览器的方式添加事件处理程序
if (supNode.addEventListener) {
supNode.addEventListener(type, handler, false);
} else if (sup.attachEvent) {
supNode.attachEvent("on" + type, handler);
} else {
supNode["on" + type] = handler;
}
//
function handler(event) {
// 采用跨浏览器的方式获取事件对象和事件目标
event = event || window.event;
var target = event.target || event.srcElement;
// 获取需要委托事件的所有子节点
var targets = Array.prototype.slice.call(supNode.querySelectorAll(selector));
// 检测事件目标是否在进行事件委托的那些子节点中
if (targets.indexOf(target) !== -1) {
/*
* 如果该事件目标对事件进行了委托,那么将事件处理程序的上下文
* 绑定到事件目标上,并将获取的事件对象作为参数传入
*/
fn.call(target, event);
}
}
}

调用

<ul id = "myLinks">
<li id = "goSomewhere">Go somewhere </li>
<li id = "doSomething">Do something </li>
<li id = "saySomething">Say something </li>
</ul>
<script type="text/javascript">
var ul = document.getElementById("myLinks");
delegateEvent(ul, "li", "click", function() {
alert(this.id);
alert(event.type);
});
</script>

3. 移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。
上文我们已经知道,可以通过事件代理技术来减少这种连接。
另外,我们还可以在不需要某些事件处理程序时,将其移除,来释放它们占用的内存空间。

那么什么时候会导致这种 “空事件处理程序” 呢,主要有两种情况:

从文档中移除带有事件处理程序的元素

移除带有事件处理程序的元素可以通过纯粹的 DOM 操作,例如 removeChild() 和 replaceChild() 方法;也可以通过使用 innerHTML 替换页面中某一部分来实现。

这两种方法都可能产生 “空事件处理程序”,但更多地发生在用第二种方法移除时。如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。

看下面的例子:

<div id = "myDiv">
<input type = "button" value = "Click me" id = "myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function() {
//先执行某些操作
document.getElementById("myDiv").innerHTML = "Processing...";
};
</script>

这里有个按钮,为了防止重复点击,在点击了一次后,按钮被移除,取而代之的是一个字符串。问题是事件处理程序并没有被移除,仍然与按钮保持着引用联系。有的浏览器(特别是 IE)可能会将对元素和对事件处理程序的引用都保存在内存中。

如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,如下面的代码所示:

<div id = "myDiv">
<input type = "button" value = "Click me" id = "myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function() {
//先执行某些操作
btn.onclick = null; // 移除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing...";
};
</script>


采用事件代理也有助于解决这个问题,如果你事先知道了将来要用 innerHTML 替换页面某一部分内容,那么就可以将该部分内的元素的事件处理程序委托给高层次的元素节点。这样,当元素被替换后,也不用担心有过多的 “空事件处理程序”

页面卸载

如果在页面被卸载之前没有清理干净事件处理程序,那么它们就会滞留在内存中。每次加载完页面再卸载页面时(刷新页面或在两个页面间切换),内存中滞留的对象数目就会增加,因为事件处理程序占用的内存并没有被释放。

一般来说,最好的做法是在页面卸载之前,先通过 onunload 事件处理程序移除所有事件处理程序。我们可以这样说:只要通过 onload 事件处理程序添加的东西,最后都要通过 onunload 事件处理程序将它们移除。

其他参考:
【1】http://www.w3cmark.com/2016/439.html