事件流
事件流描述的是从页面中接收事件的顺序。但有意思的是,IE 和 Netscape 开发团队居然提出了差不多是完全相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。
- 事件冒泡
IE 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的HTML页面为例:
Event Bubbling Example
Click Me
//如果你单击了页面中的
div=>body=>html=>document。也就是说,click 事件首先在
由于老版本的浏览器不支持,因此很少有人使用事件捕获。我们也建议大家放心地使用事件冒泡,在有特殊需要时再使用事件捕获。
- 事件捕获
Netscape Communicator 团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。如果仍以前面的 HTML 页面作为演示事件捕获的例子,那么单击元素就会以下列顺序触发 click 事件:
document=>html=>body=>div。在事件捕获过程中,document 对象首先接收到 click 事件,然后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即元素。下图展示了事件捕获的过程:
由于老版本的浏览器不支持,因此很少有人使用事件捕获。我们也建议大家放心地使用事件冒泡,在有特殊需要时再使用事件捕获。- DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发送的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。下图展示了事件流的过程:
注:
1.实际目标捕获事件被归为了事件冒泡阶段。
2.多数支持 DOM 事件流的浏览器都实现了一种特定的行为:即使“DOM2 级事件”规范明确要求捕获阶段不会涉及事件目标,但高版本浏览器都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会再目标对象上面操作事件。事件处理程序
- HTML事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。这个特性的值应该是能够执行的 JavaScript 代码。例如,要在按钮被单击时执行一些 JavaScript,可以像下面这样编写代码:
缺点:
(1)首先,存在一个时差问题。因为用户可能会在 HTML 元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。以前面的例子来说明,假设 showMessage() 函数是在按钮下方、页面的最底部定义的。如果用户在页面解析 showMessage() 函数之前就单击了按钮,就会引发错误。为此,很多HTML事件处理程序都会被封装在一个 try-catch 块中,以便错误不会浮出水面,如下面的例子所示:这样,如果在 showMessage() 函数有定义之前单击了按钮,用户将不会看到 JavaScript 错误,因为在浏览器有机会处理错误之前,错误就被捕获了。
(2)这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同 JavaScript 引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。
(3)HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。- DOM0级事件处理程序
通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写,例如 onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:
var btn = document.getElementById("myBtn"); btn.onclick = function(){ console.log("Clicked"); };
在此,我们通过文档对象取得了一个按钮的引用,然后为它指定了 onclick 事件处理程序。但要注意,在这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段时间内怎么单击都没有反应。
使用 DOM1 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。
var btn = document.getElementById("myBtn"); btn.onclick = function(){ console.log(this.id); // "myBtn" };
也可以删除通过 DOM1 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可:
btn.onclick = null; // 删除事件处理程序
- DOM2级事件处理程序
DOM2 级事件定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。
要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:
var btn = document.getElementById("myBtn"); btn.addEventListener("click", function(){ console.log(this.id); }, false);
上面的代码为一个按钮添加了 onclick 事件处理程序,而且该事件会在冒泡阶段被触发(因为最后一个参数是 false)。与 DOM1 级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。来看下面的例子。
var btn = document.getElementById("myBtn"); btn.addEventListener("click", function(){ console.log(this.id); }, false); btn.addEventListener("click", function(){ console.log("Hello world!"); }, false);
在这个例子中,我们使用 addEventListener() 添加了一个事件处理程序。虽然调用 removeEventListener() 时看似使用了相同的参数,但实际上,第二个参数与传入 addEventListener() 中的那一个是完全不同的函数。而传入 removeEventListener() 中的事件处理程序函数必须与传 入addEventListener() 中的相同,如下面的例子所示。
var btn = document.getElementById("myBtn"); var handler = function(){ console.log(this.id); }; btn.addEventListener("click", handler, false); btn.removeEventListener("click", handler, false); // 有效!
重写后的这个例子没有问题,是因为在 addEventListener() 和 removeEventListener() 中使用了相同的函数。
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,我们不建议在事件捕获阶段注册事件处理程序。- IE事件处理程序
IE 实现了与 DOM 中类似的两个方法:attachEvent() 和 detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序都会被添加到冒泡阶段。
要使用 attachEvent() 为按钮添加一个事件处理程序,可以使用以下代码。
var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ console.log("Clicked"); });
注意,attachEvent() 的第一个参数是 "onclick",而非 DOM 的 addEventListener() 方法中的 "click"。
在 IE 中使用 attachEvent() 与使用 DOM1 级方法的主要区别在于事件处理程序的作用域。在使用 DOM1 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此 this 等于 window。来看下面的例子。var btn = document.getElementById("myBtn"); btn.attachEvent("onclick", function(){ console.log(this === window); // true });
在编写跨浏览器的代码时,牢记这一区别非常重要。
与 addEventListener() 类似,attachEvent() 方法也可以用来为一个元素添加多个事件处理程序。不过,与 DOM 方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。使用 attachEvent() 添加的事件可以通过 detachEvent() 来移除,条件是必须提供相同的参数。与 DOM 方法一样,这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给 detachEvent(),就可以移除相应的事件处理程序。
- 跨浏览器的事件处理程序
为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的 JavaScript 库,还有一些开发人员会自己开发最合适的事件处理的方法。自己编写代码其实也不难,只要恰当地使用能力检测即可。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关注冒泡阶段。
var EventUtil = { addHandler: function(element, type, handler){ if (element.addEventListener){ element.addEventListener(type, handler, false); } else if (element.attachEvent){ element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } }, removeHandler: function(element, type, handler){ if (element.removeEventListener){ element.removeEventListener(type, handler, false); } else if (element.detachEvent){ element.detachEvent("on" + type, handler); } else { element["on" + type] = null; } } };
这两个方法首先都会检测传入的元素中是否存在 DOM2 级方法。如果存在 DOM2 级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false(表示冒泡阶段)。如果存在的是 IE 的方法,则采取第二种方案。注意,为了在 IE8 及更早版本中运行,此时的事件类型必须加上 "on" 前缀。最后一种可能就是使用 DOM1 级方法(在现代浏览器中,应该不会执行这里的代码)。此时,我们使用的是方括号语法来将属性名指定为事件处理程序,或者将属性设置为 null。
可以像下面这样使用 EventUtil 对象:
var btn = document.getElementById("myBtn"); var handler = function(){ console.log("Clicked"); }; EventUtil.addHandler(btn, "click", handler); EventUtil.removeHandler(btn, "click", handler);
addHandler() 和 removeHandler() 没有考虑到所有的浏览器问题,例如在 IE 中的作用域问题。不过,使用它们添加和移除事件处理程序还是足够了。
事件对象
在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。所有浏览器都支持 event 对象,但支持方式不同。
- 跨浏览器的事件对象*
虽然 DOM 和 IE 中的 event 对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。IE中 event 对象的全部信息和方法 DOM 对象中都有,只不过实现方式不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。可以对前面介绍的 EventUtil 对象加以增强,添加如下方法以求同存异。
var EventUtil = { addHandler: function(element, type, handler){ // 省略的代码 }, getEvent: function(event){ return event ? event : window.event; }, getTarget: function(event){ return event.target || event.srcElement; }, preventDefault: function(event){ if (event.preventDefault){ event.preventDefault(); } else { event.returnValue = false; } }, removeHandler: function(element, type, handler){ // 省略的代码 }, stopPropagation: function(event){ if (event.stopPropagation){ event.stopPropagation(); } else { event.cancelBubble = true; } } };
- DOM事件流