首先,大家都知道js跟html的交互是通过事件实现的,事件代表的是文档或者浏览器窗口中某个有意义的时刻。我们可以定义一个监听器或者事件处理程序去订阅事件。在传统的软件工程领域中,这种模式叫观察者模式。
事件最早是在IE3和Netscape Navigator2中出现的,当时的用意是把某些表单处理工作从服务器转移到浏览器上来,但是浏览器厂商之间没有统一的规范,事件在浏览器的发展过程中,也形成了不一样的标准。后来,DOM2开始标准化DOM事件Api,到现在为止,所有的现代浏览器都实现了DOM2Events的核心部分。IE8是最后一个使用专有事件系统的主流浏览器(所以可能会在开发中针对IE8做兼容处理)。
事件流
事件流描述了页面接受事件的顺序,但是,IE和网景又提出了完全相反的方案,IE支持事件冒泡,另一个则支持事件捕获。
事件冒泡: 事件的发生首先由最具体的元素监听到,然后向父级元素传递。
事件捕获: 由最外层元素向最具体的元素传递。
之后DOM2将事件流进行了规定,事件流分为3个阶段,事件捕获,到达目标,事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后实际的目标元素接收到事件,之后是事件冒泡,最迟也要在这个阶段响应事件。在DOM的事件流中,实际的目标元素在捕获阶段是不会接受到事件的,但是大多数支持DOM事件流的浏览器实现了一个小小的拓展,就是会在捕获阶段也会在事件目标上触发事件。最终的结果就是在事件目标上有两个机会处理事件。 注意,所有的现代浏览器都支持DOM事件流,只有IE8及更早版本不支持。
DOM0事件处理程序
DOM0中指定事件处理程序的方式主要有两种,第一种是直接在html元素上用on+‘事件的类型’的方式,例如 onclick,onmouseover等。第二种就是在js中通过拿到元素的引用,给元素的引用加一个on+‘事件的类型’的函数,例如:
let btn = document.getElementById('btn');
btn.onclick = function() {}
//注销事件处理程序的方式都是先获得元素的引用,然后将该函数设为null,
btn.onclick = null;
//事件处理程序中的this指向的是那个目标元素。
DOM2事件处理程序
DOM2Events为事件处理程序的赋值和移除定义了两个方法:addEventListener(),removeEventListener(),这两个方法可以在所有的dom节点上访问,他们接受3个参数,第一个是事件名,第二个是函数,第三个是布尔值,true代表捕获,false代表冒泡。不同与DOM0的是,DOM2可以为同一个事件添加多个事件处理程序。通过addEventListener函数注册的程序必须使用removeEventListener注销,匿名函数是无法注销的。
IE中的事件处理程序
IE实现了与DOM类似的方法,即attachEvent()和detachEvent().这两个方法接受两个一样的参数,第一个是事件类型,第二个是事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加的冒泡阶段。
var btn = document.getElementById('btn');
btn.attachEvent('onclick', function() {
console.log(this === window); //true
})
在IE中使用attachEvent与使用DOM0方式的主要区别是事件处理程序的作用域。使用DOM0方式时this指向目标元素,而attachEvent中则是指向window。理解这个差异对编写跨浏览器的代码是很重要的。IE中也可以给某个元素注册多个相同事件的处理程序,像DOM2中那样,但是跟DOM2的区别是多个相同的处理程序的执行顺序正好相反,DOM2中是先定义先触发,IE中attachEvent相反。移除事件处理程序和DOM2一样。
跨浏览器事件处理程序
为了跨浏览器兼容的方式处理事件,我们可能需要同时兼顾不同的浏览器,其实我认为更多的是要兼容IE8及之前的版本,因为它有着自己专用的一套事件机制。至此,我们可以根据上述事件处理程序的介绍,去自己编写一套兼容的代码,如下:
var event = {
addHandler(ele, type, handler) {
if (ele.addEventListener) {
ele.addEventListener(type, handler, false);
} else if (ele.attachEvent) {
ele.attachEvent("on"+type, handler);
} else {
ele["on"+type] = handler;
}
},
removeHandler(ele, type, handler){
if(ele.removeEventListener) {
ele.removeEventListener(type, handler);
} else if(ele.detachEvent) {
ele.detachEvent("on"+type, handler);
} else {
ele["on"+type] = null;
}
}
}
这里的 addHandler和removeHandler方法并没有解决所有的跨浏览器一致性问题,比如IE中的作用域问题,多个事件处理程序执行顺序问题等。不过已经实现了跨浏览器添加和移除事件处理程序。另外需要注意的是DOM0只支持给一个事件添加一个事件处理程序,还好DOM0浏览器已经很少使用了,所以影响应该不大。
事件对象
DOM中发生事件时,所有的相关信息都会被收集并存储在一个名为event的对象中。这个对象包含了一些基本的信息,比如导致事件的元素,发生的事件类型,以及可能与特定事件相关的任何其他数据,所有的浏览器都支持这个对象,尽管支持的方式不同。
DOM事件对象
在DOM合规的浏览器中,event对象是传给事件处理程序的唯一参数,不管以哪种方式指定的,都会传递它。
let btn = document.getElementById('btn');
btn.onclick = function(event){
console.log(event.type); // click
}
btn.addEventListener('click', function(event){
console.log(event.type); //click
}, false)
事件对象包含与特定事件相关的属性和方法。不同的事件生成的事件对象也会包含不同的属性和方法。不过,所有的事件对象都会包含下面几个常用的字段。
currentTarget 当前事件处理程序所在的元素
preventDefault() 用于取消事件的默认行为。
stopPropagation() 用于取消所有后续事件捕获或事件冒泡。
target 事件目标
type 事件类型
在事件处理程序内部,this对象始终等于currentTarget的值,targer只包含事件的实际目标。
IE事件对象
与DOM对象不同的是,IE 事件对象可以基于事件处理程序被指定的方式以不同的方式来访问,如果事件处理程序是使用DOM0方式指定的,则event对象只是window对象的一个属性。如果是使用attachEvent指定的,则event对象会作为唯一的参数传递给处理函数。
let btn = document.getElementById('btn');
btn.onclick = function(){
let event = window.event;
console.log(event.type);
}
//
let btn = document.getElementById('btn');
btn.attachEvent('onclick', function(event){
console.log(event.type);
})
IE对象也包含与导致其创建的特定事件相关的属性和方法。
cancelBubble 默认false,true时可以取消冒泡,和DOM的stopPropagation方法相同。
returnValue 默认为true,设置为false可以取消事件默认行为,与DOM的preventDefault相同。
srcElement 事件目标,类似DOM中的target。
由于事件处理程序的作用域取决于指定它的方式,因此this的值并不总是等于事件目标,所以更好的方式是使用事件对象的srcElement属性代替this。如下所示:
let btn = document.getElementById('ben');
btn.onclick = function(event){
console.log(window.event.srcElement === this); //true
}
btn.attachEvent('onclick', function(event){
console.log(event.srcElement === this) // false
})
跨浏览器事件对象
var eventUtil = {
addHandler(ele, type, handler) {
if (ele.addEventListener) {
ele.addEventListener(type, handler, false);
} else if (ele.attachEvent) {
ele.attachEvent("on"+type, handler);
} else {
ele["on"+type] = handler;
}
},
removeHandler(ele, type, handler){
if(ele.removeEventListener) {
ele.removeEventListener(type, handler);
} else if(ele.detachEvent) {
ele.detachEvent("on"+type, handler);
} else {
ele["on"+type] = null;
}
}
}
//下面是新加的方法
getEvent(event){
return event ? event : window.event;
}
getTarget(){
return event.target || event.srcElement;
}
preventDefault(event){
if(event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
stopPropagation(event){
if(event.stopPropagation){
event.stopPropagation();
}
else {
event.cancelBubble = true;
}
}
//使用示例
btn.onclick = function(event){
event = eventUtil.getEvent(event);
let target = eventUtil.getTarget(event);
eventUtil.preventDefault(event);
eventUtil.stopPropagation(event);
}