js中的事件流和事件对象

720 阅读7分钟

首先,大家都知道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);
     }