Zerlinda's Blog

Javascript事件处理机制详解

一、Javascript事件处理机制

当我们在页面中点击一个div元素的时候,可能仅仅只想点击div元素,而事实上我们是首先点击了document,然后点击事件通过层层元素才传递到div的。而且点击最终并不会在div停下,如果其有子元素就会继续往下传递,最后再依次回传至document。W3C规范中定义了3个事件阶段,依次是捕获阶段、目标阶段、冒泡阶段。

捕获阶段:在事件对象到达事件目标之前,事件对象必须从window经过目标的祖先节点传播到事件目标。 这个阶段被我们称之为捕获阶段。在这个阶段注册的事件监听器在事件到达其目标前必须先处理事件。

目标阶段:事件对象到达其事件目标。 这个阶段被我们称为目标阶段。一旦事件对象到达事件目标,该阶段的事件监听器就要对它进行处理。如果一个事件对象类型被标志为不能冒泡。那么对应的事件对象在到达此阶段时就会终止传播。

冒泡阶段: 事件对象以一个与捕获阶段相反的方向从事件目标传播经过其祖先节点传播到window。这个阶段被称之为冒泡阶段。在此阶段注册的事件监听器会对相应的冒泡事件进行处理。不是所有的事件都能冒泡。以下事件不冒泡:blur、focus、load、unload。

说再多还是图来的直观一点,Javascript中事件传播过程如下图:

javascript事件传播 

<div id = "test">
     id = test
    <div id = "test1">
	id = test1
    </div>
</div>

上面的代码当分别为内层的第div和外层的div绑定click事件,浏览器会如何处理呢?

事件捕获

当事件处理程序注册在事件捕获阶段,先捕获到父级元素,先触发事件,子级元素后触发。

事件冒泡

当事件处理程序注册在事件冒泡阶段,子元素首先冒泡先触发事件,父级元素后触发。

这两种事件处理顺序刚好相反。

W3C模型

W3C模型是将两者进行中和,在W3C模型中,任何事件发生时,先从顶层开始进行事件捕获,直到事件触发到达了事件根元素。然后,再从事件源往上进行事件冒泡,直到到达document。

程序员可以自己选择绑定事件时采用事件捕获还是事件冒泡,通过addEventListener函数,第三个参数若是true,则表示采用事件捕获,若是false则表示采用事件冒泡。
IE只支持事件冒泡,不支持事件捕获,它也不支持addEventListener函数,不会用第三个参数来表示是冒泡还是捕获。

就像下面的例子:

var ele = document.getElementById("test"),
ele1 = document.getElementById("test1");
ele.addEventListener("click", show, true(false));
ele1.addEventListener("click", show, true(false));
function show() {
  e = EventUtil.getEvent();
  target = EventUtil.getTarget(e);
  console.log("target's id: " + target.id + ";  this's id: " + this.id);
}
//false 事件冒泡
/*
target's id: test1;  this's id: test1
target's id: test1;  this's id: test
*/
//true  事件捕获
/*
target's id: test1;  this's id: test
target's id: test1;  this's id: test1
*/

当事件监听注册在冒泡阶段,首先触发的是内层div,而注册在捕获阶段的事件,首先触发的是外层div。

点击此处查看事件冒泡与事件捕捉示例

事件的传播是可以阻止的:

在W3c中,使用stopPropagation()方法;在IE下设置cancelBubble = true阻止事件冒泡。

在W3c中,使用preventDefault()方法;在IE下设置window.event.returnValue = false;阻止事件的默认行为,例如click <a>后的跳转;

二、Javascript事件处理程序

1、DMO0级事件处理程序

DOM Leavl 0是最早的事件处理形式,它既可以直接写在HTMl上,也可以把一个函数分配给一个事件处理程序。

(1)html事件处理程序
<input type="button" value="click me" onclick="alert("hello")" />

这种写法事件处理程序,写在相应的html标签中。缺点是当用户在html元素刚加载完就去触发相应的事件时,事件的处理程序可能还不具备执行条件(比如说调用的函数没有被解析)。比如下面这样事件处理程序在html代码中插入,而对应的事件通过script标签添加在body的底部,在message函数被加载之前就点击按钮就会引发错误。

<input type="button" value="click me" onclick="message()" />;
<script type="text/javascript">
    function message(){alert("hello world");}
</script>;

另外,在html中添加事件处理程序会导致html和js代码耦合度太高,如果改变某一事件处理程序则维护起来会很麻烦。

(2)直接通过函数添加事件处理程序
btn.onclick=function(){…};

这种事件处理程序被认为是元素的方法,直接通过函数添加的事件处理程序是在元素的作用域中运行的,所以程序中的this指向被引用的当前元素。因此在事件处理程序中通过this可以访问元素的任何属性和方法。而以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

同样删除指定的事件处理程序也很简单,只须将事件处理程序的属性设置为null就可以。

btn.onclick=null;

所有浏览器都支持DOM0级事件处理程序,且使用该方式时,事件处理程序是在元素的作用域中运行,因此程序中的this都是指向元素。但是DMO0级对同一元素的同一事件只允许一个事件处理函数。

2、DOM2级事件处理程序

DMO0级事件的优点是所有浏览器均支持,不存在兼容问题。但缺点是同一元素的同一事件只允许一个事件处理函数。所以对于DOM级别2的事件最大的好处就是一个事件可以注册许多处理器。但是需要注意的是添加的这些事件的处理程序是按逆序触发的。

(1)IE浏览器的DOM2级事件: attachEvent()和detachEvent()

这两个函数接收相同的两个参数:事件处理程序名(onclick等)与事件处理函数。由于IE只支持事件冒泡,所以通过attachEvent添加的事件处理程序都会添加到冒泡阶段。

var btn=getElementById("myBtn");
btn.attachEvent("onclick",function(){  alert(1)  });
btn.detachEvent("onclick",function(){  alert(2)  });

以上代码先弹出2在弹出1。

编写跨浏览器的代码非常重要的一点是,在IE所特有的DOM2级事件处理程序是在全局作用域中执行的,也就是说,IE在使用attachEvent方法的情况下,事件处理程序的作用域为全局作用域,因此this始终等于window。

(2)非IE浏览器的DOM2级事件:addEventListener()和removeEventListner()

这两个函数接受3个参数,分别为:事件处理程序名,处理函数,布尔值。最布尔值参数如果为ture,表示在捕获阶段处理程序,如果为false,表示在冒泡阶段调用事件处理程序。

例如在按钮上为click添加多个事件处理程序,可以用下面的代码:

var btn=getElementById("myBtn");
btn.addEventListner("click",function(){alert(1);},flase);
btn.addEventListner("click",function(){alert(2);},flase);

结果先显示2,后显示1。

通过addEventListner()添加的事件处理程序只能通过removeEventListner来删除。移除时使用的参数与添加事件处理程序的参数相同。

需要注意的是,通过addEventListner添加的匿名函数无法删除。

3、跨浏览器的事件处理程序

能力检测,即:识别浏览器的能力。要保证代码能在大多数浏览器下一致的运行,只须关注冒泡阶段。

在处理跨浏览器的事件处理程序的时候视情况判定使用DOM0级方法,DOM2级方法,以及IE方法来添加事件。函数应该接收3个参数:要操作的元素、事件名称、事件处理程序函数。最终将该函数作为方法添加到对象当中去。最后使用对象来处理浏览器之间的差异。

以下是最后的跨浏览器的事件处理程序代码:

var EventUtil = {
  getEvent: function(event) {
    return event ? event: window.event;
  },
  getTarget: function(event) {
    return event.target || event.srcElement;
  },
  getRelatedTarget: function(event) {
    if (event.relatedTarget) {
      return event.relatedTarget;
    } else if (event.toElement) {
      return event.toElement;
    } else if (event.fromElement) {
      return event.fromElement;
    } else {
      return null;
    }
  },
  addHandler: function(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type,
      function() {
        return handler.call(element, window.event);
      });
    } else {
      element["on" + type] = handler;
    }
  },
  removeListener: function(element, type, hander) {
    if (element.removeEventListener) {
      element.removeEventListener(type, hander, false);
    } else if (element.deattachEvent) {
      element.deattachEvent("on" + type,
      function() {
        return handler.call(element, window.event);
      });
    } else {
      element['on' + type] = null;
    }
  },
  preventDefault: function(event) {
    if (event.preventDefault) {
      event.preventDefault();
    } else {
      event.returnValue = false;
    }
  },
  stopPropagation: function(event) {
    if (event.stopPropagation) {
      event.stopPropagation();
    } else {
      event.cancelBubble = true;
    }
  }
};

以上代码列出里跨浏览器的事件处理程序,包括获取事件目标(event),源目标(targetevent)以及关于mouseenter和mouseout事件的相关目标(relateevent),添加删除事件以及阻止事件冒泡和阻止事件的默认行为。

发表评论

电子邮件地址不会被公开。 必填项已用*标注