最近在深刻实践js中,遇到了一些问题,好比我须要为动态建立的DOM元素绑定事件,那么普通的事件绑定就不行了,因而经过上网查资料了解到事件委托,所以想总结一下js中的事件绑定与事件委托。html
以下示例代码,经过节点属性显式声明,直接在HTML中,显式地为按钮绑定了click事件,当该按钮有用户点击行为时,便会触发myClickFunc方法。node
/* html */ <button id="btn" onclick="myClickFunc()"> ClickMe </button> /* js */ // 事件处理程序 var myClickFunc = function(evt){ // TODO.. }; // 移除事件处理程序 myClickFunc = function(){};
显而易见,这种绑定方式很是不友好,HTML代码和JS代码严重耦合在一块儿,好比当要修改一个函数名时候,就要修改两次,web
经过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;
经过事件监听的方式绑定事件,DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操做。函数
// event: 事件名称 // function: 事件函数 // boolean: false | true, true 为事件捕获, false 为事件冒泡(默认); Ele.addEventListener(event,function[,boolean]); // 添加句柄 ELe.removeEventListener(event,function[,boolean]); // 移除句柄
看个例子:this
/* 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){..});
DOM 2级事件处理程序在IE是行不通的,IE有本身的事件处理程序方法:attachEvent()
和detachEvent()
。这两个方法的用法与addEventListener()
是同样的,可是只接收两个参数,一个是事件名称,另外一个是事件处理程序的函数。为何不使用第三个参数的缘由呢?由于IE8以及更早的浏览器版本只支持事件冒泡。看个例子:spa
/* 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
。例子以下:.net
//跨浏览器事件处理程序 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);
在了解事件委托以前,要先了解下事件冒泡和事件捕获。code
早期的web开发,浏览器厂商很难回答一个哲学上的问题:当你在页面上的一个区域点击时,你真正感兴趣的是哪一个元素。这个问题带来了交互的定义。在一个元素的界限内点击,显得有点含糊。毕竟,在一个元素上的点击同时也发生在另外一个元素的界限内。例如单击一个按钮。你实际上点击了按钮区域、body元素的区域以及html元素的区域。htm
伴随着这个问题,两种主流的浏览器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节点之间的关联变少了,这样也就减小了因循环引用而带来的内存泄漏发生的几率。