函数闭包与事件委托与事件绑定什么关系?

题目

逛博客的时候遇到一个这样的问题,原以为是个很容易的题目,后来亲自动手后发现事情并不是那么简单。

1
2
3
4
5
6
7
8
9
// 点击以下列表,输出li的索引值
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>

解题

错误的方法

1
2
3
4
5
6
7
8
9
window.onload = function(){
  var parent = document.getElementById('parent');
  var children = parent.getElementsByTagName('li');
  for(var i=0,len=children.length; i<len;i++){
   children[i].onclick = function(){
console.log(i) // 总是输出6
    } 
  }
}

这就是一个经典的for循环输出问题,i是全局变量,for循环后,i就等于6,所以点击那个都是输出‘6’,那么这该怎么解决呢,最简单的方法是用ES6的let来划分作用域,使得 i 只在for循环中能访问。

用let纠正错误

1
2
3
4
5
6
7
8
9
window.onload = function(){
  var parent = document.getElementById('parent');
  var children = parent.getElementsByTagName('li');
  for(let i=0,len=children.length; i<len;i++){
   children[i].onclick = function(){
console.log(i)
    } 
  }
}

用闭包纠正错误

1
2
3
4
5
6
7
8
9
10
11
12
window.onload = function () {
var parent = document.getElementById('parent');
var children = parent.getElementsByTagName('li');
for (var i = 0, len = children.length; i < len; i++) {
//使用立即执行函数来切断闭包对外部变量i的引用
(function (i) {
children[i].addEventListener('click', function () {
console.log(i)
}, false)
})(i)
}
}

出现的问题

按照以上两种方法确实可以解决问题,但是问题来了,如果说有10000个li元素,那岂不是要绑定一万个事件,对象越多,内存占用率就越大,这对性能优化并不友好,或者说li的数量是不确定的,那么后来添加的li是没有绑定事件的,所以,不管从正确性还是性能来讲,以上用事件绑定的方法是不可靠的,这时,我们就可以用事件委托机制来完美解决这一问题。

更好的办法:事件委托

什么是事件委托

JavaScript高程的解释:

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

为什么要使用事件委托

一般来说,dom需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的dom需要添加事件处理呢?比如我们有10000个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,这样浏览器中就含有10000个事件对象,内存早晚会爆表。

如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能

事件委托的原理

先弄清楚事件冒泡,用网上一张图来解释,当我们点击li时,事件会从根结点一层层进行捕获,然后到达点击的目标,最后再一层层向上冒泡。
image

  • 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件;
  • 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上;
  • 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层

当子元素的事件冒泡到父ul元素时,你可以检查事件对象的target属性,捕获真正被点击的节点元素的引用。

实现

1
2
3
4
5
6
7
8
9
10
window.onload = function () {
var parent = document.getElementById('parent');
var children = parent.getElementsByTagName('li');
parent.addEventListener('click', function (e) {
var target = e.target;
if (target.nodeName.toLowerCase() === 'li') {
console.log([].indexOf.call(children, target));
}
}, false);
}

总结

如果遇到需要像大量元素绑定事件的场景,用事件委托是非常优雅的做法,不但减少内存消耗,还能避免因为dom的变更导致事件失效的问题,即能动态地绑定事件,可以减少很多重复工作。