详解事件委托
前言
之前一直都听过事件委托和事件代理,但是总是讲不清楚它是什么,最近看到了博友的文章,受益匪浅,于是按照自己的思路整理一下,以便日后查看。
原文链接:http://www.cnblogs.com/liugang-vip/p/5616484.html
第一部分:理解事件委托
例子:
有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。
这里实际上是由两层意思的:
- 第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的;
- 第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的
第二部分:实例讲解
(1)又如:假设一个ul下有5个li,点击每个li会有一个相同的操作,如提示: “clicked”,我们通常的做法是使用for循环,为每一个li添加事件。如下所示:
<ul id="ull"> <li>li1</li> <li>li2</li> <li>li3</li> <li>li4</li> </ul> <script> var ull = document.getElementById("ull"), lis = ull.getElementsByTagName("li"),
i; for (i = 0; i < lis.length; i++) { lis[i].onclick = function () { alert("clicked"); } } </script>
但是li如果有100个,1000个呢? 我们就要大量的使用for循环,大量的操纵DOM,而这样又是非常耗时的。
但是事件委托就能很好地解决这个问题,它的原理是什么呢?
事件委托利用事件冒泡,即在上面的例子中我点击了 ul下的li, 如果ul上也有点击事件,那么这个点击就会冒泡到ul上,从而出发ul的点击事件。即委托父级办事。
注:事件冒泡可以看我的文章《事件冒泡》
也就是说,对于上面的问题,我们可以直接在ul上绑定一个事件,如下所示:
<ul id="ull"> <li>li1</li> <li>li2</li> <li>li3</li> <li>li4</li> </ul> <script> var ull = document.getElementById("ull"), lis = ull.getElementsByTagName("li"); ull.onclick = function () { alert("clicked"); } </script>
这样我们就节省了大量的DOM操作并完成了相同的效果,Perfect!
(2) 上面的操作是点击了li之后都实现了同样地效果,但是如果我们希望在点击了之后能有不同的效果该怎么实现呢?
先看一看我们平时不用事件委托是怎么实现的, 即每点击一个li,就会alert它的innerHTML,这里使用了闭包,因为点击事件在没有执行以前,它的作用域中对立即执行匿名函数中的i有引用,所以在循环之后虽然都立即执行了还会保留。
<ul id="ull"> <li>li1</li> <li>li2</li> <li>li3</li> <li>li4</li> </ul> <script> var ull = document.getElementById("ull"), lis = ull.getElementsByTagName("li"); for (var i = 0; i < lis.length; i++) { (function(i){ lis[i].onclick = function () { alert(this.innerHTML); } }(i)) } </script>
显然,上面操作dom的方法是非常消耗性能的,并且大量的使用了闭包,闭包也会导致大量消耗内存的问题,一般情况下,我们是不建议使用闭包的,除非像这样的特殊情况。
但是如果使用事件委托就可以很容易的解决这个问题,如下所示:
<ul id="ull"> <li>li1</li> <li>li2</li> <li>li3</li> <li>li4</li> </ul> <script> var ull = document.getElementById("ull"); ull.onclick = function(e) { var e = e || window.event; var target = e.target || e.srcElement; if (target.nodeName.toLowerCase() === "li") { alert(target.innerHTML); } } </script>
即我们先判断点击的元素是li,然后利用这个强大的target属性来获取到点击的元素,然后再获取innerHTML。这样做的好处在于避免了for循环以及闭包使用,他们都是非常消耗性能的。
要理解上面的例子,我们必须得清楚 || 和 && 的应用:
- 逻辑或(expr1 || expr2) --- 如果两者都在Boolean环境中使用,只要有一个是true就返回true,若都是false就返回false。 如果两者至少有一个不是在Boolean环境中使用,那么expr1能转换为true就返回true,否则返回expr2。
- 逻辑与(expr1 && expr2) --- 如果两者都在Boolean环境下使用,必须两者都是true才能返回true,若有一个是false,就返回false。如果至少有一个不是在Boolean环境中使用,那么expr1能转化为false就返回expr1,否则返回expr2。其实这里还是比较好理解的,对于 || 第一个可以转化为true,就返回第一个,这个无可厚非; 而对于 && ,第一个可以转化为true,显然就需要去判断 第二个了,所以直接返回第二个; 如果第一个就是转化为false了,那么总的来说一定是false,所以就没有比较第二个的必要,所以直接返回第一个。
- 逻辑非(!expr) --- 如果单个表达式是true或能转化成true,就返回false; 如果单个表达式是false或能转化为false,就返回true。
另外,这里我们使用到了nodeName属性,即返回某个dom元素的名称,一般返回的是大写,我们可以先转化成小写再作比较。如下所示:
(3)上面我们所说的情况都是在li的数量固定的情况下,但是如果li是动态改变的呢?
先来看看我们一般的做法:
<button id="addBtn">添加新的元素</button> <ul id="ull"> <li>li1</li> <li>li2</li> <li>li3</li> <li>li4</li> </ul> <script> var ull = document.getElementById("ull"), lis = ull.getElementsByTagName("li"),
i; for (i = 0; i < lis.length; i++) { (function(i){ lis[i].onclick = function () { alert(this.innerHTML); } }(i)) } var index = 4, button = document.getElementById("addBtn"); button.onclick = function () { index++; var newLi = document.createElement("li"); newLi.innerHTML = "li" + index; ull.appendChild(newLi); } </script>
这段代码很容易理解,这是不可能给新添加的元素添加click的事件的,因为for循环只是把最开始的li元素循环了一遍,而以后再添加的时候却没有循环,所以解决方法如下:
var ull = document.getElementById("ull"), lis = ull.getElementsByTagName("li"); function traversalLi () { for (var i = 0; i < lis.length; i++) { (function(i){ lis[i].onclick = function () { alert(this.innerHTML); } }(i)) } }; traversalLi(); var index = 4, button = document.getElementById("addBtn"); button.onclick = function () { index++; var newLi = document.createElement("li"); newLi.innerHTML = "li" + index; ull.appendChild(newLi); traversalLi(); };
这种方法当然是可行的,但是缺点是每添加一个元素,就会循环创建一遍函数,这是非常消耗性能的。
那么用事件委托的方法怎么实现呢?如下所示(在html添加了一个button元素用于添加新的li元素):
var ull = document.getElementById("ull"), button = document.getElementById("addBtn"); ull.onclick = function(e) { var e = e || window.event, target = e.target || e.srcElement; if (target.nodeName.toLowerCase() === "li") { alert(target.innerHTML); } } var index = 4; button.onclick = function () { index++; var newLi = document.createElement("li"); newLi.innerHTML = "li" + index; ull.appendChild(newLi); }
即不管你是原有的元素还是新添加的元素只要被点击了就会冒泡(除非你使用DOM2级事件,且把其第三个参数设置为true),这样,父元素就会受到委托来帮你干这件事情。
(4)上面的例子演示的都是li中直接就是文字的情况,但是在实际项目中这是不可能的! 那么如果html的结构如下,我们该怎么使用事件委托处理呢?
<ul id="ull"> <li><p>这是p中的文字</p></li> <li><div><span>这是span中的文字</span></div></li> <li><div><h3>这是h3中的文字</h3></div></li> <li><h2>这是h2中的文字</h2></li> </ul>
但是对于这种情况呢? 我们希望弹出li中的文字应该如何做呢? 因为我们每次点击的时候很有可能点击的是span或p或div或h3或者是h2,当然也有可能是li了。 但是点中li是需要概率的,要是这样的话,你的用户早就该没了。
解决这个问题有两个方法:
方法一: 将innerHTML替换成innerText, 并取消 if (target.nodeName.toLowerCase() === "li") 的判断,这样无论点击到哪里得到的就是li中的元素了。
如下所示:
var ull = document.getElementById("ull"), button = document.getElementById("addBtn"); ull.onclick = function(e) { var e = e || window.event, target = e.target || e.srcElement; alert(target.innerText); } var index = 4; button.onclick = function () { index++; var newLi = document.createElement("li"); newLi.innerHTML = "li" + index; ull.appendChild(newLi); }
存在的问题: 这种方法确实可以解决问题,但是如果环境变了,我们再给li添加一个id,我们点击到每一个元素的时候,还希望获取到他的id,这时用上面的方法显然就不行了。
方法二: 每次点击获取到li,然后使用li的innerText,这样如果li有不同的id,我们也就可以获得li的id了。
var ull = document.getElementById("ull"), button = document.getElementById("addBtn"); ull.onclick = function(e) { var e = e || window.event, target = e.target || e.srcElement; while (target !== ull) { if (target.nodeName.toLowerCase() === "li") { alert(target.innerText); break; } target = target.parentNode; } } var index = 4; button.onclick = function () { index++; var newLi = document.createElement("li"); newLi.innerHTML = "li" + index; ull.appendChild(newLi); }
ok! 这样的方法还是非常不错的。
第三部分:总结
值得注意的是,click事件是可以冒泡的,所以我们可以使用,另外,keydown、keyup、mousedown、mouseup、mouseover、mouseout这些事件都是可以冒泡的也可以使用。而和mouseover与mouseout非常相似的mouseenter与mouseleave由于不能冒泡所以不可使用这种方法。
最后,我们可以看到使用事件委托的方法对于性能优化还是有不小的帮助的,并且可以解决我们很多的问题。 所以对于项目中的代码就可以好好花时间重构了~