JS事件处理机制及事件代理(事件委托)实例详解
作者:flying_huixia
一、先记个小知识点。cssText
cssText 本质:设置 HTML 元素的 style 属性值。
用法:document.getElementById("d1").style.cssText= "color:red; font-size:13px;";
cssText 返回值:在某些浏览器中(比如 Chrome),你给他赋什么值,它就返回什么值。在 IE 中则比较痛苦,它会格式化输出、会把属性大写、会改变属性顺序、会去掉最后一个分号,比如:
cssText的使用优势:样式一多,代码就很多;而且通过JS来覆写对象的样式是比较典型的一种销毁原样式并重建的过程,这种销毁和重建,都会增加浏览器的开销。语法为:obj.style.cssText=”样式”;这样就可以尽量避免页面reflow,提高页面性能。
但是,这样会有一个问题,会把原有的cssText清掉,比如原来的style中有’display:none;’,那么执行完上面的JS后,display就被删掉了。为了解决这个问题,可以采用cssText累加的方法:
Element.style.cssText += 'width:100px;height:100px;top:100px;left:100px;'
注意:上面cssText累加的方法在IE中是无效的。解决办法是,可以在前面添加一个分号来解决这个问题:
Element.style.cssText += ';width:100px;height:100px;top:100px;left:100px;'
补充:如果前面有样式表文件写着 div {text-decoration:underline; },这个会被覆盖吗?不会!因为它不是直接作用于 HTML元素的 style 属性。
二、JS的事件处理机制
1、事件流:指从页面中接收事件的顺序,有冒泡流和捕获流。
2、DOM2级事件规定事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。首先发生的是事件捕获,然后是实际的目标接收道事件,最后是冒泡阶段,可以在这个阶段对事件做出响应。
分析:实际的(text)元素在捕获阶段不会接收到事件,意味着在捕获阶段,事件从document到<body>再到<div>后就停止了。下一个阶段是“处于目标阶段”,于是事件在(text)上发生,并在事件处理中被看成是冒泡阶段的一部分。最后,冒泡阶段发生,事件又传播回文档。
3、事件处理程序
诸如click,load,mouseover都是事件的名字,而响应某个事件的函数就是事件处理程序(事件侦听器)。事件处理程序的名字以on开头,比如onclick.onmouseover等。
(1)HTML事件处理程序:某个元素支持的每种事件,都可以用一个相应事件处理程序同名的HTML特性来决定。
<input type="button" value="click" οnclick="alert('clicked')"/>
<input type="button" value="click" οnclick="alert(event.type)"/>
第二动态创建的函数中会有一个局部变量event,也就是事件对象。通过event变量,可以直接访问事件对象。
另外,这个动态创建的函数扩展作用域的方式如下:使用with
在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。
function(){ with(documnet){ with(this){ /元素属性值 } } }
(2)DOM0级事件处理程序
基于DOM0的事件,对于同一个dom节点而言,只能注册一个,后边注册的 同种事件 会覆盖之前注册的。利用这个原理我们可以解除事件,btn5.onclick=null;其中this就是绑定事件的那个元素;
这里添加的事件处理程序是在其依附的元素的作用域中运行。
DOM0级对每个事件只支持一个事件处理程序。
(3)DOM2级事件处理程序
DOM2支持同一dom元素注册多个同种事件,事件发生的顺序按照添加的顺序依次触发(IE是相反的)。DOM2事件通过addEventListener和removeEventListener管理。 DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener(eventName,handlers,boolean)和removeEventListener(),两个方法都一样接收三个参数, 要处理的事件名,第二个是 事件处理程序函数,第三个值为 布尔值。
布尔值是true,表示在捕获阶段调用事件处理程序。false时表示在事件冒泡阶段调用事件处理程序,一般建议在冒泡阶段使用,特殊情况才在捕获阶段; 注意:通过addEventListener() 添加的事件处理程序只能用removeEventListener() 来移除,并且移除时传入的参数必须与添加时传入的参数一样;通过addEventListener()添加的匿名函数将无法移除。(js高程P351-P352)
使用DOM2级事件处理程序可以添加多个事件处理程序:
var btn2 = document.getElementById('btn2');
var handlers = function () { console.log(this.id); }; btn2.addEventListener('click',handlers,false); btn2.addEventListener("click",function(){alert("hello")},false); btn2.removeEventListener('click',handlers.false);
这里为按钮添加了两个事件处理程序,他们会按照添加他们的顺序触发。
(4)IE事件处理程序
IE用了attachEvent(),和detachEvent(),接收两个参数,事件名称和事件处理程序函数。由于IE8及以前只支持事件冒泡;通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。所以平时为了兼容更多的浏览器最好将事件添加到事件冒泡阶段。
var btn3 = document.getElementById('btn3'); var handlers2=function(){ console.log(this===window);//true,注意attachEvent()添加的事件处理程序运行在全局作用域中; }; btn3.attachEvent('onclick',handlers2);
分析:attachEvent()的第一个参数是“onclick”DOM则是“click”
重点:在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行。因此this等于window。
attachEvent()也可以为同一元素添加两个不同的事件处理程序。只是执行事件时以相反的顺序被触发。
(5)跨浏览器事件处理程序
为了以跨浏览器的方式处理事件,有两个方法,addHandler(),它的职责是视情况分别使用DOM0和DOM2或者IE方法来添加或删除事件。这个方法属于一个名叫EventUtil的对象,可以处理浏览器差异。这个方法接收三个参数。要操作的元素、事件名称、和事件处理程序函数。对应的方法是removeHandler()函数,它的职责是移除事件处理程序。默认采用DOM0级方法。
4 事件对象
触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含了所有与事件有关的信息,比如导致事件的元素target,事件的类型,及其他特定的相关信息。例如鼠标操作导致的事件对象中会包含鼠标的位置,单双击等,而键盘操作导致的事件对象会包含按下的键等信息;
事件被触发时,会默认给事件处理程序传入一个参数e , 表示事件对象;通过e,我们可以获得其中包含的与事件有关的信息; 只有在事件处理程序执行期间,event对象才会存在,一旦事件处理程序执行完毕,event对象就会被销毁;
(1)DOM中的事件对象
兼容DOM的浏览器会自动将一个事件对象event传递给事件处理程序。
在通过HTML特性指定事件处理函数时,变量event中保存着event对象。event对象包含与创建它的特定事件的有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。
currentTarget | 只读 | 事件处理程序当前正在处理事件的那个元素 |
datail | 只读 | 与事件相关的细节 |
eventPhase | 只读 | 调用事件处理程序的阶段1 捕获阶段 2 处于目标 3 冒泡阶段 |
target | 只读 | 事件的目标 |
type | 只读 | 被触发的事件的类型 |
在事件处理程序内部,对象this始终等于currentTarget的值,target包含事件的实际目标。
ps:关于事件对象中的this,target,currentTarget,看个例子:(注:event.target不支持IE浏览器,应该用event.srcElement;还有 IE中通过attachment添加的事件是运行在全局作用域中的,this===window。
preventDefault() 阻止事件的默认行为,只有cancelabel属性的值设为true时,才可以使用preventDefalut. |
event.stopPropagation()可以阻止事件的传播.,取消进一步的事件冒泡或者捕获 |
(2)IE中的事件对象
要访问IE的event对象有几种不同的方式,取决于指定事件处理程序的方法。
比如使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在,因此可以通过window.event来访问event对象。
var btn=document.getElementById("myBtn"); btn.οnclick=function(){ var event=window.event; alert(event.type); }
输出结果是click.
如果是使用attachEvent()来添加事件处理程序,那么会有一个对象作为参数传入事件处理程序函数中。
var btn=document.getElementById("myBtn"); btn.attachEvent("onclick",function(event){ alert(event.type); });
输出结果是click.
IE中event对象同样包含与创建它的事件相关的方法和属性。其中很多属性和方法都有对应的或者相关的DOM属性和方法。这些属性和方法会因为事类型的不同而不同。
srcElement | 只读 | 事件的目标(与DOM中target属性相同) |
type | 只读 | 被触发的事件的类型 |
cancelBubble | 读/写 | 默认值为false,设置为true可以取消事件冒泡(与DOM中stopPropagation()一样) |
returnValue | 读/写 | 默认为true,设置为false,就可以阻止默认行为。(与DOM中的preventDefault()一样) |
将returnValue设置为false,就可以阻止默认行为。 |
cancelBubble属性值为true,可以取消事件冒泡。 |
(3)跨浏览器的事件对象
虽然DOM和IE中对象不同,但基于二者之间的相似性依旧可以拿出跨浏览器的方案来。
IE中的event中的全部信息和方法都是类似的只是实现方式不同,可以用前面提到过的EventUtil对象来求同存异。
var EventUtil(){ addHandler:function(element,type,handler){ //省略代码 }, getEvent:function(event){ return event?event:window.event; }, getTarget:function(event){ return event.target||event.srcElement; }, preventDefault:function(event){ if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue=false; } }, removeHandler:function(element,type,handler){ //省略代码 }, stopPropagation:function(event){ if(event.stopPropagation){ event.preventDefault(); }else{ event.cancelBubble=true; } } };
以上代码为EventUtil 添加了4个方法;getEvent(),返回event对象的引用。其它方法类似。
5 事件委托
因为冒泡机制,比如既然点击子元素,也会触发父元素的点击事件,那我们完全可以将子元素的事件要做的事写到父元素的事件里,也就是将子元素的事件处理程序写到父元素的事件处理程序中,这就是事件委托;利用事件委托,只指定一个事件处理程序,就可以管理某一个类型的所有事件;
通俗来说:事件委托是利用事件的冒泡原理来实现的,何为事件冒泡呢?就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>ul>li>a;比如给最里面的a加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序a>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。
示例1:
<ul> <li>111</li> <li>222</li> <li>333</li> <li>444</li> </ul>
实现点击li出现123.
传统方法:
window.onload = function(){ var oUl = document.getElementById("ul1"); var aLi = oUl.getElementsByTagName('li'); for(var i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(123); } } }
使用事件委托:
window.onload = function(){ var oUl = document.getElementById("ul1"); oUl.onclick = function(){ alert(123); } }
这里用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发,当然,这里当点击ul的时候,也是会触发的,那么问题就来了,如果我想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击li才会触发???
示例2:
Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement。
window.onload = function(){ var oUl = document.getElementById("ul1"); oUl.onclick = function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'li'){ alert(123); alert(target.innerHTML); } } }
这样,只有点击li才会触发事件。
示例3
对比下列两段代码实现:
window.onload = function(){ var oBtn = document.getElementById("btn"); var oUl = document.getElementById("ul1"); var aLi = oUl.getElementsByTagName('li'); var num = 4; //鼠标移入变红,移出变白 for(var i=0; i<aLi.length;i++){ aLi[i].onmouseover = function(){ this.style.background = 'red'; }; aLi[i].onmouseout = function(){ this.style.background = '#fff'; } } //添加新节点 oBtn.onclick = function(){ num++; var oLi = document.createElement('li'); oLi.innerHTML = 111*num; oUl.appendChild(oLi); }; }
注意:这里添加的新节点并不会有事件处理程序。
window.onload = function(){ var oBtn = document.getElementById("btn"); var oUl = document.getElementById("ul1"); var aLi = oUl.getElementsByTagName('li'); var num = 4; //事件委托,添加的子元素也有事件 oUl.onmouseover = function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'li'){ target.style.background = "red"; } }; oUl.onmouseout = function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if(target.nodeName.toLowerCase() == 'li'){ target.style.background = "#fff"; } }; //添加新节点 oBtn.onclick = function(){ num++; var oLi = document.createElement('li'); oLi.innerHTML = 111*num; oUl.appendChild(oLi); }; }
用事件委托的方式,新添加的子元素是带有事件效果的,我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。
示例4: 点击某一个 Li 标签时,将 Li 的背景色显示在 P 标签内,并将 P 标签中的文字颜色设置成 Li 的背景色
传统实现:
var list = document.querySelectorAll("li"); for (var i = 0, len = list.length; i < len; i++) { list[i].onclick = function(e) { var t = e.target; var c = t.style.backgroundColor; var p = document.getElementsByClassName("color-picker")[0]; p.innerHTML = c; p.style.color = c; } }
运用事件委托:
var ulist=document.getElementsByClassName("palette")[0]; ulist.οnclick=function(ev){ var ev = ev || window.event; var target = ev.target || ev.srcElement; if (target.nodeName.toLowerCase() === 'li') { var c = target.style.backgroundColor; var p = document.getElementsByClassName("color-picker")[0]; p.innerHTML = c; p.style.color = c; } }
注意:ul只有一个,要用索引,[0],如果不写,无法实现。
总结一下js委托相关的:
- 因为把事件绑定到了父节点上,因此省了绑定事件。就算后面新增的子节点也有了相关事件,删除部分子节点不用去销毁对应节点上绑定的事件
- 父节点是通过event.target来找对应的子节点的。(事件处理程序中的this值始终等于currentTarget的值,指向的是绑定到的那个元素)