事件流
事件流用简单的一句话描述就是:事件触发的顺序问题。可以先用一个简单的小例子直观的感受一下什么是事件流。
<div id="wrapper">
<div id="content"></div>
</div>
<script>
var wrapperDom = document.getElementById("wrapper");
var contentDom = document.getElementById("content");
wrapperDom.addEventListener('click',function(){
console.log("click wrapper");
});
contentDom.addEventListener('click',function(){
console.log("click content");
});
</script>
然后我们点里面的id为content的Div,目前我们共同的认知是 wrapper和content的点击事件都会触发,但是问题就在于是先触发外层的还是里层的呢?这就是JS的事件流问题,也就是刚才说的事件触发顺序问题。下面附上运行结果(点击content)
Output:click content
click wrapper
从运行的结果来看,content比wrapper先触发点击事件。
事件流历史
目前有两种事件流,一种是IE提出的冒泡流
,还有一种是网景公司提出的捕获流
,W3C统一后,JS同时支持这两种事件流,不过IE8及以下版本都只支持冒泡流,所以冒泡流更广为应用,为了更好地兼容低版本浏览器,建议使用冒泡流。
事件流分析
下图就对为什么上面的例子会先输出content后输出wrapper做了简单的演示:
我们可以根据这个图来分析事件流的整个过程,首先事件流被分为了3个阶段,分别是:捕获过程
、目标过程
、冒泡过程
。事件流从window开始,最后再window结束。
①1~5:这是捕获过程,字面意思就是去捕获我们的触发事件的DOM元素,按照上面的例子,也就是找到我们点击的contentDom。
②5~6:这是目标过程,也就是找到目标元素后,在这个阶段触发事件,这里也就是触发了contentDom的点击事件。
③6~10:这是冒泡过程,简单理解就是从目标元素一层一层往上询问,你是否要触发事件?如果要,则触发,最终回溯到window为止。
通过上面的分析,我们也就能够明白为什么之前的例子,会先触发content再触发wrapper了。
但是! 并不是所有的情况都能按照我们想象的过程完美推进,这就要说到另外一个概念了,就是DOM Level
,其实我们上面的例子是使用的DOM Level2
,究竟什么是Dom Level?
DOM Lelvel
先用一个表格简单了解下有哪些DOM Level:
DOM Level | 捕获事件 | 冒泡事件 |
---|---|---|
DOM Level 0 | 不支持 | 支持 |
DOM Level 2 | 支持 | 支持 |
DOM Level 3 | 支持 | 支持 |
关于为什么没有DOM Level1?因为DOM Level1标准中并没有定义事件相关的内容,所以没有所谓的DOM Level1事件模型。
Dom Level 0
与Dom Level 2
和 Dom Level 3
最大的区别就在于Dom Level 0不支持在捕获阶段触发事件,仅支持在冒泡阶段触发。而Level2/3 都是同时支持两种的。
DOM Level 0
定义Level 0 的事件有两种形式,一种是直接写在html标签中
,另一种是使用onclick
这种方式:
方式1:
<div id="wrapper" onclick="console.log(123);">
方式2:
<script>
var wrapperDom = document.getElementById("wrapper");
wrapperDom.onclick = function(){
console.log(123);
}
</script>
DOM Level 0 事件还有一个特点就是事件会被覆盖掉,什么意思呢,看下面的代码:
<div id="wrapper">
<div id="content"></div>
</div>
<script>
var wrapperDom = document.getElementById("wrapper");
wrapperDom.onclick = function(){
console.log("click wrapper");
}
wrapperDom.onclick = function(){
console.log("click wrapper again");
}
</script>
按照我们的想象,应该会将两个事件同时输出,但是事实上只输出了后绑定的事件:
Output:click wrapper again
所以,我们想要解除某个元素上绑定的事件,我们只需要这样做:
wrapperDom.onclick = null;
DOM Level 2
DOM Level 2 既支持事件捕获也支持事件冒泡的,但是由于部分浏览器不支持事件捕获,所以还是尽量使用事件冒泡。DOM Level 2事件提供了两个方法供我们绑定和解绑:addEventListener()
和removeEventListener()
。并且Level2支持一个元素绑定多个事件,看下面代码:
<div id="wrapper">
<div id="content"></div>
</div>
<script>
var wrapperDom = document.getElementById("wrapper");
var contentDom = document.getElementById("content");
wrapperDom.addEventListener('click',function(){
console.log("click wrapper");
});
contentDom.addEventListener('click',function(){
console.log("click content");
});
contentDom.addEventListener('click',function(){
console.log("click content again");
});
</script>
如上,我们给contentDom绑定了两个click事件,我们来看看输出结果:
Output:
click content
click content again
click wrapper
addEventListener()
可以传入三个参数,分别是:事件名
、事件函数
、是否使用捕获事件
。
第三个参数是一个布尔值,true
代表选择捕获阶段触发事件,false
则代表冒泡触发。第三个参数缺省是false。下面的代码就是使用捕获阶段触发:
<div id="wrapper">
<div id="content"></div>
</div>
<script>
var wrapperDom = document.getElementById("wrapper");
var contentDom = document.getElementById("content");
wrapperDom.addEventListener('click',function(){
console.log("click wrapper");
},true);
contentDom.addEventListener('click',function(){
console.log("click content");
},true);
contentDom.addEventListener('click',function(){
console.log("click content again");
},true);
</script>
再来看看输出结果:
Output:
click wrapper
click content
click content again
看到输出结果我们便知,这些事件都是在捕获阶段就触发了。
然后,我们通过addEventListener()
添加事件处理程序的程序只能使用removeEventListener()
来移除。但是值得注意的是不能使用匿名函数来移除,否则是无效的:
wrapperDom.addEventListener('click',function(){
console.log("click wrapper");
});
wrapperDom.removeEventListener("click",function(){
console.log("click wrapper");
})
这样子是无法移除这个事件的,点击之后依然会触发之前绑定的事件,我们需要如下这么做:
function handle(){
console.log("click wrapper");
}
wrapperDom.addEventListener('click',handle);
wrapperDom.removeEventListener("click",handle);
这么做就可以成功的移除我们绑定的事件了。
值得注意的是:DOM Level 2在IE中的绑定事件是attachEvent
,解除绑定是detachEvent
,在标准的浏览器绑定事件是addEventListener
,解除绑定是removeEventListener
。
扩展
我们可以在控制台打印一个事件,这里我打印的是click的事件:
对标红的的属性做一些说明:
属性名 | 说明 |
---|---|
bubbles | 事件是否冒泡 |
cancelable | 是否可以取消事件默认行为 |
currentTarget | 当前正在处理事件的元素 |
defaultPrevented | 是否已经调用了preventDefault()方法 |
detail | 事件相关的细节信息 |
eventPhase | 表示事件处理阶段,1捕获阶段 2目标阶段 3冒泡阶段 |
target | 事件的目标元素 |
type | 事件的类型 |
view | 与事件关联的抽象视图, 相当于发生事件的window对象 |
还有几个重要的函数
函数名 | 说明 |
---|---|
preventDefault() | 取消事件的默认行为 |
stopImmediatePropagation() | 取消事件的进一步捕获或者冒泡, 同时阻止任何事件处理程序被调用 |
stopPropagation() | 取消事件的进一步捕获或者冒泡 |
preventDefault()
preventDefault()函数是用于阻止原标签事件触发的,示例代码:
<a href="http://www.baidu.com" id="tag">跳转百度</a>
<script>
var tagDom = document.getElementById("tag");
tagDom.onclick = function(event) {
console.log("click a tag");
event.preventDefault();
}
</script>
如果我们不使用preventDefault()
方法,就会同时跳转到百度并打印内容,但是如果加上了这个方法,就只会打印不会跳转,也就是阻止了标签本身的一些事件。
注意 在IE浏览器上面是event事件是没有preventDefault()这个属性的,所以在IE上,我们需要设置的属性是returnValue
window.event.returnValue=false;
stopPropagation()
这个方法还是非常常用的,适用于阻止事件冒泡的,实例代码:
<div id="wrapper">
<div id="content"></div>
</div>
<script>
var wrapperDom = document.getElementById("wrapper");
var contentDom = document.getElementById("content");
wrapperDom.onclick = function(){
console.log("click wrapper");
}
contentDom.onclick = function(event){
console.log("click content");
event.stopPropagation();
}
</script>
如果我们不使用stopPropagation()
方法,就会依次输出click content、click wrapper,但是如果在contentDom的点击事件加上这个方法,就可以阻止事件继续往上冒泡,也就不会冒泡到wrapperDom上了。因此只会执行contentDom对应的事件。
注意 在IE浏览器上面通过设置cancelBubble的值:
event.cancelBubble=true;
事件委托
事件委托也叫事件代理,是一种前端性能优化的手段,其原理是利用了事件冒泡。举个简单的例子,我有下面这样一个DOM结构:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
按照我们之前的做法,我们应该要获取每一个li的Dom,然后分别绑定上事件,但是这么做会有大量的DOM的操作,是非常消耗性能的。那么根据之前的说法,我只需要给ul绑定上事件,每次点击li然后事件会冒泡到ul上,再执行ul绑定的事件。这样就大大减少了DOM操作。并且除了DOM操作,事件的绑定也是非常消耗性能的。直接看例子:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<script>
var ulDom = document.getElementsByTagName("ul")[0];
ulDom.onclick = function(e){
var e = e || window.event;
var target = e.target || e.srcElement; //srcElement是IE的说法
if(target.nodeName.toLowerCase() == 'li'){
console.log(target.innerHTML);
}
}
</script>
如果我们想给每个li执行不同的事件,我们可以给li加上id,通过id的判断去执行不同的事件。