JS编程中经常接触到DOM事件,虽然只是事件而已,可是处于历史缘由,它是足够复杂的,也足够强大的,因为本身以前老是对DOM事件的一些概念模糊不清,因此这里来作一下总结啦html
咱们常听到DOM0级事件,DOM1级事件,DOM2级事件处理,其实这里的DOM0,DOM1,DOM2和DOM级别是密切相关的。node
最开始的时候,IE4和Netscape等浏览器各自实现本身的一套DOM api,没有一个统一的标准,对于开发者和用户来讲是一件头疼的事,因而,1998年,W3C综合了各家的现有API,制定了DOM1级(DOM Level 1)标准。DOM1级比较简洁,由两个模块组成:DOM核心(DOM Core)和DOM HTML,其中各个事件相关的信息做为方法包含在在DOM元素中进行说明的,详细能够了解W3C的相关章节.编程
DOM2级的目标要宽泛不少,引入了多个新模块,其中包括DOM Events,即在DOM2的时候,单独把事件做为一个模块,并针对DOM1级事件部分做了很大的升级,再也不只是简单的告诉咱们有哪些事件能够用,而是使事件的标准更加详细,如新增了事件流,事件捕获,事件冒泡,事件取消等机制和规范。api
DOM Level 3并无对事件作任何修订(多是由于认为DOM Events已经足够成熟了吧),因此咱们知道如今用的仍是DOM Level 2的事件标准。浏览器
到这里能够作一下总结,DOM API 是有DOM1级,DOM2级,DOM3级三个标准的,与之对应的每个标准中DOM事件相关的部分咱们叫作DOM1级事件处理,DOM2级事件处理(刚才也说了,DOM3级事件处理不存在),那么标准是从DOM Level 1开始的,咱们说的DOM0又是什么呢?能够说这只是公认的一种说法,即在有事实标准以前的事件处理,咱们叫作DOM0级事件处理。bash
由于DOM1只是对以前各大厂商的DOM api作了如下整理而实施的标准,因此一般咱们认为DOM1的事件处理和DOM0的事件处理是同样的。函数
事件处理程序就是响应某个事件的函数,DOM中的事件处理程序有多种方式,大概能够分为如下三种类型。工具
<button onclick="alert(hello world!)"></hello>
复制代码
像上述代码这样,直接将事件函数写到HTML中元素的属性上,就是HTML事件处理程序,这里双引号中的部分是事件触发后要执行的代码,它其实是由JS引擎由eval()
调用的,因此它是全局做用域。ui
这样的事件处理有一个明显的缺点,即当JS代码太复杂时,将大段JS代码卸载HTML中显然不合适,因而有了下面这种写法:this
<button onclick="doSomething()"></hello>
复制代码
这样虽然解决了嵌套代码过长的问题,但又引来了另外一个问题,即时差问题—,若是用户在界面刚出现就进行点击,而JS尚未加载好的话,就会报错。
此外,很重要的一点是,这种写法,一个函数的改变,可能同时须要js和html的改变,严重违背了轻耦合的原则,综上,咱们有了DOM0级事件处理。
<script>
var btn=document.getElementById("#btn");
btn.onclick=function(){
alert(hello world!)
}
</script>
复制代码
能够看到,这种方式中能够把事件处理相关部门都放到js中,而且这里的事件处理程序是做为btn对象的方法的,是局部做用域。
可是如今,我依然面临着问题,若是有对这个元素的单击事件添加两个处理函数,这个就没法帮我实现了,并且即便不须要添加多个处理函数,我也不太敢轻易的添加事件,除非我很是肯定,别人写代码时不会涉及到这部分(由于一不当心可能会覆盖他人以前对这个元素的该事件添加的处理函数)。
进一步规范后,有了DOM2级事件处理程序,咱们能够经过相似以下代码,对一个元素的同一个事件添加多个处理程序
var btn=document.getElementById("#btn");
btn.addEventListener("click",function(){
alert(hello world!)
})
btn.addEventListener("click",function(){
alert(hello world2!)
})
</script>
复制代码
经过DOM2级的addEventListener
方法咱们能够实现绑定多个事件处理程序,但要注意的是一样的事件和事件流机制下相同的方法只会触发一次,即相同的方法会发生覆盖。 等等,这里的事件流又是什么呢?
funcgrand(),funcparent(),funcchild()
,那么当我在儿子上单击时,哪一个函数会被触发呢?
首先来分析一下,若是说直观感觉是在儿子元素上发生的单击事件,因此应该触发funcchild()
,但细细想来这样是不妥的,由于儿子元素自己就是父亲元素甚至爷爷元素的一部分,因此说是否是至关于也在父亲和爷爷元素上发生了单击事件呢?答案是是的,这种状况下三个元素绑定的对应事件的函数都会被浏览器触发,那么问题又来啦,既然三个函数都会被触发,那么它们应该以什么顺序被触发呢,是自上到下呢,仍是自下到上呢?
这个问题也就是咱们常说的事件流了,即元素从页面中接收事件的顺序,也即事件在页面中的传播顺序。
W3C对这个问题给了咱们一个答案,就是均可以,既能够自上而下依次触发,又能够自下而上触发,具体顺序由咱们本身而定(之因此支持这两种方式,是为了与以前浏览器的实现兼容,由于早期IE事件传播方向为从上至下,而Netscape 则从下至上)。
实际上,以前咱们提到的addEventListener
还有第三个参数,能够为true
或false
.当第三个参数为true
时,绑定的是捕获阶段的事件,在捕获阶段,事件是由上到下依次触发的,反之当第三个参数为false
时,绑定的是冒泡阶段的事件,在冒泡阶段,事件是由下到上触发的。
W3C规定,当事件发生时,最早通知window,而后是document,由上到下依次进入知道最底层的被触发的那个元素(也就是目标元素,一般的event.target
的值)为止,这个过程就是捕获。 以后,事件会从目标元素开始,冒泡,由下至上逐层传递至window,这个过程就是冒泡。
因此,捕获是会比冒泡先执行的
正如事件捕获和事件冒泡提到的,事件程序可能会在两个阶段中被执行,即捕获中和冒泡中,当一个事件添加了两个处理函数,一个指定了参数true
,一个指定的参数false
,则它们都会被执行,且参数为true
的那个先执行,由于是捕获阶段先发生.
可是有一个例外,即若是事件函数被添加在了目标元素自己上,如以前的例题中的儿子元素上被绑定了两个单击事件函数,一个第三个参数是true
,一个第三个参数是false
,则它们的实际执行顺序是不受第三个参数控制的,而只是单纯的和添加事件的顺序有关(先addEventListener
的先执行),这个多是和处于目标阶段有关(目标阶段和捕获阶段和冒泡阶并称为三大阶段,因此说目标阶段中要把捕获和冒泡的思想排除?真正的顺序是捕获—>目标阶段->冒泡吧?)
对于IE来讲,在IE9以前,必须使用attachEvent
而不是标准方法addEventListener
,IE事件处理程序中有相似于DOM2级事件处理程序的2个方法attachEvent
和detachEvent
它们都接收两个参数:
事件处理程序名称,如 onclick
,onmounseover
,注意,这里是事件处理程序名称,而不是事件名称,要有前缀on
事件处理程序函数
不像DOM2级事件处理程序同样,它们不接收第三个参数,由于IE8及更早版本只支持冒泡事件流(没有捕获阶段)
在IE8中,事件执行的顺序不是添加的顺序而是添加顺序的相反顺序,而在IE6,7中 事件执行的顺序是随机的,和添加顺序无关。
使用attachEvent方法还有个缺点是,this的值会变成window对象的引用而不是触发事件的元素。
就像上述提到的,老的IE浏览器的事件处理程序不一样于标准的DOM2事件处理,因此为了兼容各浏览器的事件处理,咱们能够用一个封装的工具函数来实现通用的添加,移除事件。
var EventUtil={
addEventHandler: function(type,element,handler){
if(element.addEventListener){
element.addEventListener(type,handler,false);
}else if(element.attachEvent){
element.attachEvent("on"+type,element);
}else{
element["on"+type]=handler;
}
},
removeEventHandler: function(type,element,handler){
if(element.removeEventListener){
element.removeEventListener(type,handler,false);
}else if(element.detachEvent){
element.detachEvent("on"+type,element);
}else{
element["on"+type]=null;
}
}
}
复制代码
事件对象是用来记录一些事件发生时的相关信息的对象,但事件对象只有事件发生时才会产生,而且只能在事件处理函数内部访问,在全部事件处理函数结束后,事件对象会被销毁。
标准的Event对象属性主要有如下几个:
- bubbles 布尔值,表示事件是不是冒泡类型
- cancelable 布尔值,表示事件是否能够取消默认动做
- currentTarget 当前目标元素,即添加当前事件处理程序的元素
- target 实际目标元素,即实际触发事件的元素
- type 返回当前事件的名称
- eventPhase 事件传播的当前阶段,1表示捕获阶段
标准的Event对象的方法主要有如下几个:
- preventDefault() 通知浏览器不要执行该事件的默认动做,经常使用于阻止连接的跳转,表单的提交,等标签的默认行为
- stopPropagation() 冒泡阶段下,阻止事件的继续向上冒泡
和事件处理程序同样,事件对象的属性和方法也存在兼容性问题。
window.event
来获取,解决方式以下:function getEvent(event){
event = event || window.event
}
function hander(event){
event = getEvent(event)
...
}
复制代码
IE浏览器的event
事件没有preventDefault()
这个方法,可是能够经过设置event
的returnValue
值为false
来达到一样的效果,以下:
window.event.returnValue=false;
复制代码
IE浏览器的event
对象也没有stopPropagation()
方法,但能够设置cancelBubble
属性为true
,阻止事件的继续传播,以下:
window.event.cancelBubble=true;
复制代码
事件委托就是利用事件冒泡,只需指定一个事件处理程序,就能够管理某一类型的全部事件,经过事件委托,能够作到经过在祖先元素添加一个事件处理程序,就能够控制其子孙元素的某些行为。
需求是未ul下的全部li添加click事件对应的行为处理,在没有用事件委托以前,代码是着这样的:
<ul>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ul>
<script>
var list=document.getElementsByTagName("li");
for(i=0;i<list.length;i++){
list[i].onclick=function(){
alert("我是"+e.target);
}
}
</script>
复制代码
目前确实达到了,可以全部li都能对click事件有所响应了,但若是再添加一个添加列表项的按钮呢?当动态的添加列表项时,列表项元素被添加了,但是新添加的节点是没有绑定事件的(除非在添加元素时再加上绑定事件的逻辑),到这里,咱们发现了问题所在:
- 在全部元素上一一添加事件绑定会致使频繁的操做DOM获取元素,同时多个元素各自监听本身的事件,都会增长浏览器的消耗
- 在页面中动态添加元素时,还须要从新走一遍添加监听事件的逻辑才能使新元素可以响应事件
庆幸的是,针对这个问题,咱们有更好的解决方案,即利用冒泡的原理实现的事件委托。
咱们只监听最外层元素,而后在事件处理函数中根据事件源,即target
属性,进行不一样的事件处理,这样,咱们只须要针对一个元素添加事件处理程序,极大的下降了DOM访问,而且不须要单独为动态添加的元素添加监听事件了,由于元素的事件会冒泡到最外层,被最外层的事件处理程序截获,以下:
var ul=document.getElementById('ulList');
ul.onclick=function(e){
var e= e || window.event;
var target = e.target || e.srcElement;
if(target.nodeName.toLowerCase() === "li"){
alert("我是"+e.target);
}
}
复制代码
从这个例子能够看出,当用事件委托的时候,彻底不须要遍历元素的子节点,只须要给父级元素添加事件监听就行了,以后新添加的子节点也可以一样的对触发事件做出适当的响应
- 不是全部事件都是能够委托的。适合用事件委托的事件有:
click mousedown mouseup keydown keyup keypresss