前言:之前的上传图片用到了event.target,但是后来仔细思考了一下,自己对event.target,this,event.currentTarget的区别完全不清楚,然后发现越学越多,为了搞清楚这个问题,把DOM事件,事件流,事件捕获,事件冒泡等全部学了一遍,收获颇丰,特别总结记录下来。
事件
事件是文档或者浏览器窗口中发生的,特定的交互瞬间。
事件是用户或者浏览器自己执行的动作,比如click(用户左键单击鼠标),load(页面加载完成),JavaScript可以通过绑定触发事件来和DOM进行交互(当然也可以直接操作DOM),由于在底层JavaScript和DOM是独立的,所以多次绑定进行绑定操作会影响页面的性能(后边会提到解决方案:事件委托)。
事件流
事件流描述的是从页面中接收事件的顺序。
以下开始以例子详细分析事件流。
HTML:
<div>
<ul>
<li></li>
</ul>
</div>
CSS:
*{
margin: 0;
padding: 0;
}
div{
width: 300px;
height: 300px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -150px;
margin-left: -150px;
background-color: #2578b5;
border-radius: 50%;
}
ul{
width: 200px;
height: 200px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: -100px;
background-color: #f2de76;
border-radius: 50%;
}
li{
list-style: none;
width: 100px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -50px;
background-color: #afc8ba;
border-radius: 50%;
}
效果如下:
如图所示,思考一下:如果我们点击内层圆,就仅仅点击了内层圆吗?
很明显,我们不止点击了内层圆,而且点击了中层圆,外层圆,html,body和document。这就引出了事件流的详细定义:
事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流。
那么问题又来了,如果我们给这些元素都绑定事件,那么这些事件的执行顺序是什么?
两种事件流模型
- 冒泡型事件流:事件的传播是从最特定的事件目标到最不特定的事件目标。即从DOM树的叶子到根。
- 捕获型事件流:事件的传播是从最不特定的事件目标到最特定的事件目标。即从DOM树的根到叶子。
例子中的事件传播顺序:
- 在冒泡型事件流中,是li > ul > div > body > html > document。
- 在捕获型事件流中,是document > html > body> div > ul > li。
实际中的事件流并没有完全按照标准事件流实现, 所有现代浏览器都支持事件冒泡,但在具体实现中略有差别:
- IE5.5及更早版本中事件冒泡会跳过html元素(从body直接跳到document)。
- IE9、Firefox、Chrome、和Safari则将事件一直冒泡到window对象。
- IE9、Firefox、Chrome、Opera、和Safari都支持事件捕获。尽管DOM标准要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。
- 由于老版本浏览器不支持,很少有人使用事件捕获。建议使用事件冒泡。
实际使用中的DOM事件流
之所以会存在两种事件流,是由于微软和网景之间的竞争造成的,幸运的是,W3C决定组合使用这两种方法,并且大多数新浏览器都遵循这两种事件流方式,所以现在完整的DOM事件流分为3个阶段:
- 事件捕获阶段
- 目标阶段(事件在目标上发生并处理,但事件处理被认为发生在冒泡阶段)
- 事件冒泡阶段
尽管理论上(DOM2中规定)事件捕获阶段不会涉及事件目标,但是IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两次机会在目标对象上面操作事件。
并非所有的事件都会经过冒泡阶段 。所有的事件都要经过捕获阶段和处于目标阶段,但是有些事件会跳过冒泡阶段,如获得输入焦点的focus事件和失去输入焦点的blur事件。
在网上找到了一张原型图,但是出处没找到,感谢无名氏同学。
默认情况下,事件使用冒泡事件流,不使用捕获事件流。然而,在现代浏览器中(IE9+,Chrome,Firefox),你可以显式的指定使用捕获事件流,方法是在注册事件时传入useCapture参数,将这个参数设为true。
下面我们来测试绑定事件的不同方式会有什么区别。
DOM事件绑定
DOM0
通过javascript制定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理属性。
优点:当前所有浏览器均支持,简单且具有跨浏览器的优势。
缺点:一个事件处理程序只能对应一个处理函数。
下面我们用之前的同心圆例子对DOM0事件绑定进行测试:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.onclick = function(){
console.log("div");
}
ul.onclick = function(){
console.log("ul");
}
li.onclick = function(){
console.log("li");
}
此时我们点击内层圆,控制台输出如下:
可以看到,输出的顺序是从内到外,所以可以得出结论,DOM0绑定事件是在冒泡阶段执行的。
删除DOM0事件处理程序,只要将对应事件属性置为null即可。如将div上绑定事件删除:div.onclick = null
。
另外DOM0还有个很神奇的特性,如果我们像这样绑定div的点击事件:
div.onclick = function(){
console.log(this);
}
那么输出的this是div,也就是执行该方法的DOM对象,但是如果定义一个函数,然后在HTML中绑定,那么输出的this是window,这是由于在HTML中绑定相当于动态绑定,所以定义函数的this永远都是window,不会随着上下文改变。
DOM1
DOM1级主要定义的是HTML和XML文档的底层结构。DOM2和DOM3级别则在这个结构的基础上引入了更多的交互能力,也支持了更高级的XML特性。为此DOM2和DOM3级分为许多模块(模块之间具有某种关联),分别描述了DOM的某个非常具体的子集。
DOM2
DOM2级事件绑定方式指定了,添加事件绑定程序和删除事件绑定程序的方法。
addEventListener(ev,fn,useCapture);
removeEventListener(ev,fn,useCapture);
优点:可以在同一DOM对象绑定多个相同事件,可以控制是在捕获阶段触发还是在冒泡阶段触发。
缺点:IE8及以下不支持这种写法,而是使用独有的绑定多事件方法,所以需要自己写兼容模式(之后会提到)。
还是之前同心圆的例子来测试。
第三个参数为空(即默认的false)的情况:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.addEventListener("click",function(){
console.log("div");
});
ul.addEventListener("click",function(){
console.log("ul");
});
li.addEventListener("click",function(){
console.log("li");
});
点击内层圆,结果如下:
第三个参数为true的情况:
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.addEventListener("click",function(){
console.log("div");
},true);
ul.addEventListener("click",function(){
console.log("ul");
},true);
li.addEventListener("click",function(){
console.log("li");
},true);
点击内层圆,结果如下:
可以看到,第三个参数为空(false)则在冒泡阶段触发,第三个参数为true则在捕获阶段触发。
如果我们给同一个DOM对象同时在捕获和冒泡阶段绑定同一个类型的事件,还是同心圆的例子,给div绑定DOM0的click事件,DOM2的click事件(捕获阶段触发,冒泡阶段触发),如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.onclick = function(){
console.log("dom0 冒泡");
}
div.addEventListener("click",function(){
console.log("dom2 捕获");
},true);
div.addEventListener("click",function(){
console.log("dom2 冒泡");
},false);
点击内层圆,输出结果如下:
可以看到,先触发捕获事件,然后触发冒泡事件,并且DOM0和DOM2的事件互不影响,谁先绑定就先执行。
点击中层圆,输出结果如下:
结果和点击内层圆一样,没毛病。
点击外层圆,输出结果如下:
很神奇,外层圆的事件顺序不是按照事件流了,这是为什么呢。其实原因在于一直没提的目标(target)阶段。我们给外层圆绑定点击事件,点击内层圆,实际上的target是内层圆,中层圆同理。但是如果我们点击外层圆,外层圆自己就是target,这时就不分事件捕获和事件冒泡了,谁先绑定谁先执行。
addEventListener和removeEventListener有几点需要注意:
- 如果使用匿名函数的方式执行addEventListener,则无法使用removeEventListener删除该绑定事件。
- 如果使用具名函数的方式addEventListener,则该函数内部的this指向执行该方法的DOM对象,另外匿名函数也是指向执行该方法的DOM对象。
- 如果addEventListener和removeEventListener第三个参数不同,则不认为是同一个事件,即removeEventListener不可以删除addEventListener绑定的事件。
IE8及以下不支持标准的addEventListener和removeEventListener,而是使用了私有方法attachEvent和detachEvent。值得注意的是,这种方法的第一个参数需要加on。
attachEvent(ev,fn);
detachEvent(ev,fn);
使用之前同心圆的例子,在IE8测试,代码如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
div.attachEvent("onclick",function(){
console.log("div");
});
ul.attachEvent("onclick",function(){
console.log("ul");
});
li.attachEvent("onclick",function(){
console.log("li");
});
结果如下:
吐槽一下,IE8及以下不支持
border-radius
属性,所以已经不能算是同心圆了。
由输出结果可以看出,attachEvent会在冒泡阶段触发。
attachEvent和detachEvent也有几点需要注意:
- 使用匿名函数作为第二个参数的attachEvent是无法被删除的。
- 无论是使用具名函数还是匿名函数作为第二个参数,函数内部的this都会指向window。
其实我本身很反感兼容低版本IE的事情,也感谢我司对前端兼容性的要求是IE9+,但是毕竟不是每个公司都像我司一样,甚至有时候都不考虑IE9了,所以还是写一下兼容写法。还是之前同心圆的例子,代码如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
function addEvent(element,type,callback){
if(element.addEventListener){
element.addEventListener(type,callback,false);
}else if(element.attachEvent){
element.attachEvent('on' + type,callback);
}
}
addEvent(div,'click',function(){
console.log("绑定点击事件1");
})
addEvent(div,'click',function(){
console.log("绑定点击事件2");
})
在Chrome上输出结果如下:
在IE8上输出结果如下:
还是有些区别的,输出顺序这个到现在我还是没想明白,很奇怪,所以如果对顺序有要求,就还是放弃IE8及以下吧。
另外就是要注意,使用addEvent这个函数的时候,匿名函数的this在不同的浏览器是有区别的,总之IE依然是个大坑。
DOM3
DOM浏览器中可能发生的事件有很多种,不同事件类型具有不同的信息,DOM3级事件规定了一下几种事件:
- UI事件,当用户与页面上的元素交互时触发。
- 焦点事件,当元素获得或者失去焦点时触发。
- 鼠标事件,当用户通过鼠标在页面上执行操作时触发。
- 滚轮事件,当使用鼠标滚轮(或类似设备)时触发。
- 文本事件,当在文档中,输入文本时触发。
- 键盘事件,当用户通过键盘在页面上执行操作时触发。
- 合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发。
- 变动事件,当底层Dom结构发生变化时触发。
DOM3级事件模块在DOM2级事件的基础上重新定义了这些事件,也添加了一些新事件。包括IE9在内的主流浏览器都支持DOM2级事件,IE9也支持DOM3级事件。
另外DOM3级还定义了自定义事件,自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。
事件流的target,currentTarget和this
说了这么久,终于说到了当初写这篇博客的起因了,为了弄清楚target,currentTarget和this,不断的查资料,然后发现不只是弄清楚了这三者的区别,还对整个事件流有了初步的认识,是时候重拾起只看完第七章的《JavaScript高级程序设计》恶补基础了。
target在事件流的目标阶段。currentTarget在事件流的捕获,目标及冒泡阶段。只有当事件流处在目标阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,target指向被单击的对象而currentTarget指向当前事件活动的对象(注册该事件的对象)(一般为父级)。this指向永远和currentTarget指向一致(只考虑this的普通函数调用)。
我们来进行测试,还是同心圆的例子,首先只考虑W3C标准的浏览器(此处是Chrome),代码如下。
JavaScript:
var div = document.querySelector("div");
div.onclick = function(ev){
console.log(ev.target.nodeName);
console.log(this.nodeName);
console.log(ev.currentTarget.nodeName);
}
点击内层圆,输出结果如下:
点击中层圆,输出结果如下:
点击外层圆,输出结果如下:
可以得出结论,在W3C标准的浏览器上,ev.target指向的是事件流的target,而currentTarget和this的指向保持一致,指向当前事件活动的对象。
如果涉及到兼容性问题,兼容IE8,那么在IE8上会有一些问题,首先需要使用target的兼容写法,其次IE8的event对象上是没有currentTarget属性的。
因为各个浏览器的事件对象不一样, 把主要的事件对象的属性和方法列出来:
属性/方法 | 介绍 |
---|---|
bubble | 表明事件是否冒泡 |
cancelable | 表明是否可以取消冒泡 |
currentTarget | 当前时间程序正在处理的元素, 和this一样的 |
defaultPrevented | false ,如果调用了preventDefualt这个就为真了 |
detail | 与事件有关的信息(滚动事件等等) |
eventPhase | 如果值为1表示处于捕获阶段, 值为2表示处于目标阶段,值为三表示在冒泡阶段 |
target or srcElement | 事件的目标 |
trusted | 为ture是浏览器生成的,为false是开发人员创建的(DOM3) |
type | 事件的类型 |
view | 与元素关联的window, 我们可能跨iframe |
preventDefault() | 取消默认事件 |
stopPropagation() | 取消冒泡或者捕获 |
stopImmediatePropagation() | (DOM3)阻止任何事件的运行 |
IE下的事件对象是在window下的,而标准应该作为一个参数, 传为函数第一个参数。IE的事件对象定义的属性跟标准的不同,如:
属性/方法 | 介绍 |
---|---|
cancelBubble | 默认为false, 如果为true就是取消事件冒泡 |
returnValue | 默认是true,如果为false就取消默认事件 |
srcElement | 这个指的是target, Firefox下的也是srcElement |
言归正传,使用同心圆的例子,并且使用兼容写法,代码如下。
JavaScript:
var div = document.querySelector("div");
var ul = document.querySelector("ul");
var li = document.querySelector("li");
function addEvent(element,type,callback){
if(element.addEventListener){
element.addEventListener(type,callback,false);
}else if(element.attachEvent){
element.attachEvent('on' + type,callback);
}
}
addEvent(div,'click',function(ev){
var event = ev || window.event;
var target = event.target || event.srcElement;
console.log(target.nodeName);
if(event.currentTarget){
console.log(event.currentTarget.nodeName);
}else{
console.log("IE8及以下不支持currentTarget");
}
console.log(this.nodeName);
});
在W3C标准的浏览器上,点击内层圆,中层圆和外层圆,输出结果与之前只考虑W3C标准的浏览器的结果相同。
在IE8上,点击内层圆,输出结果如下:
点击中层圆,输出结果如下:
点击外层圆,输出结果如下:
可以看到,target是和W3C浏览器结果保持一致的,IE8及以下不支持currentTarget,
另外attachEvent的this指向window,没有nodeName这个属性,所以一直是undefined。