《JavaScript 闯关记》之事件

JavaScript 程序采用了异步事件驱动编程模型。在这种程序设计风格下,当文档、浏览器、元素或与之相关的对象发生某些有趣的事情时,Web 浏览器就会产生事件(event)。例如,当 Web 浏览器加载完文档、用户把鼠标指针移到超连接上或敲击键盘时,Web 浏览器都会产生事件。若是 JavaScript 应用程序关注特定类型的事件,那么它能够注册当这类事件发生时要调用的一个或多个函数。请注意,这种风格并不仅应用于 Web 编程,全部使用图形用户界面的应用程序都采用了它,它们静待某些事情发生(即,它们等待事件发生),而后它们响应。javascript

请注意,事件自己并非一个须要定义的技术名词。简而言之,事件就是 Web 浏览器通知应用程序发生了什么事情,这种在传统软件工程中被称为观察员模式。html

事件流

当浏览器发展到第四代时(IE4 及 Netscape Communicator 4),浏览器开发团队遇到了一个颇有意思的问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,能够想象画在一张纸上的一组同心圆。若是你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的全部圆。两家公司的浏览器开发团队在看待浏览器事件方面仍是一致的。若是你单击了某个按钮,他们都认为单击事件不只仅发生在按钮上。换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。java

事件流描述的是从页面中接收事件的顺序。但有意思的是,IE 和 Netscape 开发团队竟然提出了差很少是彻底相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。git

事件冒泡

IE 的事件流叫作事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,而后逐级向上传播到较为不具体的节点(文档)。如下面的HTML页面为例:github

<!DOCTYPE html>
<html>
<head>
    <title>Event Bubbling Example</title>
</head>
<body>
    <div id="myDiv">Click Me</div>
</body>
</html>复制代码

若是你单击了页面中的 <div> 元素,那么这个 click 事件会按照以下顺序传播:编程

  1. <div>
  2. <body>
  3. <html>
  4. document

也就是说,click 事件首先在 <div> 元素上发生,而这个元素就是咱们单击的元素。而后,click 事件沿 DOM 树向上传播,在每一级节点上都会发生,直至传播到 document 对象。下图展现了事件冒泡的过程。浏览器

事件捕获

Netscape Communicator 团队提出的另外一种事件流叫作事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预约目标以前捕获它。若是仍之前面的 HTML 页面做为演示事件捕获的例子,那么单击 <div> 元素就会如下列顺序触发 click 事件。微信

  1. document
  2. <html>
  3. <body>
  4. <div>

在事件捕获过程当中,document 对象首先接收到 click 事件,而后事件沿 DOM 树依次向下,一直传播到事件的实际目标,即 <div> 元素。下图展现了事件捕获的过程。less

因为老版本的浏览器不支持,所以不多有人使用事件捕获。咱们也建议你们放心地使用事件冒泡,在有特殊须要时再使用事件捕获。dom

事件处理程序

事件就是用户或浏览器自身执行的某种动做。诸如 clickloadmouseover,都是事件的名字。而响应某个事件的函数就叫作事件处理程序(或事件侦听器)。事件处理程序的名字以 "on" 开头,所以 click 事件的事件处理程序就是 onclickload 事件的事件处理程序就是 onload。为事件指定处理程序的方式有好几种。

HTML 事件处理程序

某个元素支持的每种事件,均可以使用一个与相应事件处理程序同名的 HTML 特性来指定。这个特性的值应该是可以执行的 JavaScript 代码。例如,要在按钮被单击时执行一些 JavaScript,能够像下面这样编写代码:

<input type="button" value="Click Me" onclick="console.log('Clicked')" />复制代码

当单击这个按钮时,就会在控制台打印 "Clicked"。这个操做是经过指定 onclick 特性并将一些 JavaScript 代码做为它的值来定义的。因为这个值是 JavaScript,所以不能在其中使用未经转义的 HTML 语法字符,例如和号(&)、双引号("")、小于号(<)或大于号(>)。为了不使用 HTML 实体,这里使用了单引号。若是想要使用双引号,那么就要将代码改写成以下所示:

<input type="button" value="Click Me" onclick="console.log(&quot;Clicked&quot;)" />复制代码

在 HTML 中定义的事件处理程序能够包含要执行的具体动做,也能够调用在页面其余地方定义的脚本,以下面的例子所示:

<script type="text/javascript">
    function showMessage(){
        console.log("Hello world!");
    }
</script>
<input type="button" value="Click Me" onclick="showMessage()" />复制代码

在这个例子中,单击按钮就会调用 showMessage() 函数。这个函数是在一个独立的 <script> 元素中定义的,固然也能够被包含在一个外部文件中。事件处理程序中的代码在执行时,有权访问全局做用域中的任何代码。

这样指定事件处理程序具备一些独到之处。首先,这样会建立一个封装着元素属性值的函数。这个函数中有一个局部变量 event,也就是事件对象:

<!-- 输出 "click" -->
<input type="button" value="Click Me" onclick="console.log(event.type)">复制代码

经过 event 变量,能够直接访问事件对象,你不用本身定义它,也不用从函数的参数列表中读取。

在这个函数内部,this 值等于事件的目标元素,例如:

<!-- 输出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(this.value)">复制代码

如此一来,事件处理程序要访问本身的属性就简单多了。下面这行代码与前面的例子效果相同:

<!-- 输出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(value)">复制代码

不过,在 HTML 中指定事件处理程序有三个缺点。首先,存在一个时差问题。由于用户可能会在 HTML 元素一出如今页面上就触发相应的事件,但当时的事件处理程序有可能尚不具有执行条件。之前面的例子来讲明,假设 showMessage() 函数是在按钮下方、页面的最底部定义的。若是用户在页面解析 showMessage() 函数以前就单击了按钮,就会引起错误。为此,不少HTML事件处理程序都会被封装在一个 try-catch 块中,以便错误不会浮出水面,以下面的例子所示:

<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">复制代码

这样,若是在 showMessage() 函数有定义以前单击了按钮,用户将不会看到 JavaScript 错误,由于在浏览器有机会处理错误以前,错误就被捕获了。

第二个缺点是,这样扩展事件处理程序的做用域链在不一样浏览器中会致使不一样结果。不一样 JavaScript 引擎遵循的标识符解析规则略有差别,极可能会在访问非限定对象成员时出错。

第三个缺点是,HTML 与 JavaScript 代码紧密耦合。若是要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的缘由所在。

DOM1 级事件处理程序

经过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,并且至今仍然为全部现代浏览器所支持。缘由一是简单,二是具备跨浏览器的优点。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操做的对象的引用。

每一个元素(包括 windowdocument)都有本身的事件处理程序属性,这些属性一般所有小写,例如 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"
};复制代码

单击按钮显示的是元素的 ID,这个 ID 是经过 this.id 取得的。不只仅是 ID,实际上能够在事件处理程序中经过 this 访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

也能够删除经过 DOM1 级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 便可:

btn.onclick = null;     // 删除事件处理程序复制代码

将事件处理程序设置为 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);复制代码

这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,所以首先会显示元素的 ID,其次会显示 "Hello world!" 消息。

经过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着经过 addEventListener() 添加的匿名函数将没法移除,以下面的例子所示。

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
    console.log(this.id);
}, false);
btn.removeEventListener("click", function(){ // 没有用!
    console.log(this.id);
}, 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九、Firefox、Safari、Chrome 和 Opera 支持 DOM2 级事件处理程序。

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(),就能够移除相应的事件处理程序。

支持 IE 事件处理程序的浏览器有 IE 和 Opera。

跨浏览器的事件处理程序

为了以跨浏览器的方式处理事件,很多开发人员会使用可以隔离浏览器差别的 JavaScript 库,还有一些开发人员会本身开发最合适的事件处理的方法。本身编写代码其实也不难,只要恰当地使用能力检测便可。要保证处理事件的代码能在大多数浏览器下一致地运行,只需关注冒泡阶段。

第一个要建立的方法是 addHandler(),它的职责是视状况分别使用 DOM1 级方法、DOM2 级方法或 IE 方法来添加事件。这个方法属于一个名叫 EventUtil 的对象,本书将使用这个对象来处理浏览器间的差别。addHandler() 方法接受3个参数:要操做的元素、事件名称和事件处理程序函数。

addHandler() 对应的方法是 removeHandler(),它也接受相同的参数。这个方法的职责是移除以前添加的事件处理程序——不管该事件处理程序是采起什么方式添加到元素中的,若是其余方法无效,默认采用 DOM1 级方法。

EventUtil 的用法以下所示。

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 中的事件对象

兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。不管指定事件处理程序时使用什么方法(DOM1 级或 DOM2 级),都会传入 event 对象。来看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.type);     // "click"
};
btn.addEventListener("click", function(event){
    console.log(event.type);     // "click"
}, false);复制代码

这个例子中的两个事件处理程序都会弹出一个警告框,显示由 event.type 属性表示的事件类型。这个属性始终都会包含被触发的事件类型,例如 "click"(与传入 addEventListener()removeEventListener() 中的事件类型一致)。

在经过 HTML 特性指定事件处理程序时,变量 event 中保存着 event 对象。请看下面的例子。

<input type="button" value="Click Me" onclick="console.log(event.type)"/>复制代码

以这种方式提供 event 对象,可让 HTML 特性事件处理程序与 JavaScript 函数执行相同的操做。

event 对象包含与建立它的特定事件有关的属性和方法。触发的事件类型不同,可用的属性和方法也不同。不过,全部事件都会有下表列出的成员。

  • bubbles,代表事件是否冒泡。
  • cancelable,代表是否能够取消事件的默认行为。
  • currentTarget,其事件处理程序当前正在处理事件的那个元素。
  • defaultPrevented,为 true 表示已经调用了 preventDefault()(DOM3 级事件中新增)。
  • detail,与事件相关的细节信息。
  • eventPhase,调用事件处理程序的阶段:1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段。
  • preventDefault(),取消事件的默认行为。若是 cancelabletrue,则可使用这个方法。
  • stopImmediatePropagation(),取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(DOM3 级事件中新增)。
  • stopPropagation(),取消事件的进一步捕获或冒泡。若是 bubblestrue,则可使用这个方法。
  • target,事件的目标。
  • trusted,为 true 表示事件是浏览器生成的。为 false 表示事件是由开发人员经过 JavaScript 建立的(DOM3 级事件中新增)。
  • type,被触发的事件的类型。
  • view,与事件关联的抽象视图,等同于发生事件的 window 对象。

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。若是直接将事件处理程序指定给了目标元素,则 thiscurrentTargettarget 包含相同的值。来看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.currentTarget === this);    // true
    console.log(event.target === this);           // true
};复制代码

这个例子检测了 currentTargettargetthis 的值。因为 click 事件的目标是按钮,所以这三个值是相等的。若是事件处理程序存在于按钮的父节点中(例如 document.body),那么这些值是不相同的。再看下面的例子。

document.body.onclick = function(event){
    console.log(event.currentTarget === document.body);  // true
    console.log(this === document.body);                 // true
    console.log(event.target === document.getElementById("myBtn"));  // true
};复制代码

当单击这个例子中的按钮时,thiscurrentTarget 都等于document.body,由于事件处理程序是注册到这个元素上的。然而,target 元素却等于按钮元素,由于它是 click 事件真正的目标。因为按钮上并无注册事件处理程序,结果 click 事件就冒泡到了 document.body,在那里事件才获得了处理。

在须要经过一个函数处理多个事件时,可使用 type 属性。例如:

var btn = document.getElementById("myBtn");
var handler = function(event){
    switch(event.type){
        case "click":
            console.log("Clicked");
            break;
        case "mouseover":
            event.target.style.backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor = "";
            break;
    }
};
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;复制代码

这个例子定义了一个名为 handler 的函数,用于处理3种事件:clickmouseovermouseout。当单击按钮时,会出现一个与前面例子中同样的警告框。当按钮移动到按钮上面时,背景颜色应该会变成红色,而当鼠标移动出按钮的范围时,背景颜色应该会恢复为默认值。这里经过检测 event.type 属性,让函数可以肯定发生了什么事件,并执行相应的操做。

要阻止特定事件的默认行为,可使用 preventDefault() 方法。例如,连接的默认行为就是在被单击时会导航到其 href 特性指定的 URL。若是你想阻止连接导航这一默认行为,那么经过连接的 onclick 事件处理程序能够取消它,以下面的例子所示。

var link = document.getElementById("myLink");
link.onclick = function(event){
    event.preventDefault();
};复制代码

只有 cancelable 属性设置为 true 的事件,才可使用 preventDefault() 来取消其默认行为。

另外,stopPropagation() 方法用于当即中止事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡。例如,直接添加到一个按钮的事件处理程序能够调用 stopPropagation(),从而避免触发注册在 document.body 上面的事件处理程序,以下面的例子所示。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log("Clicked");
    event.stopPropagation();
};
document.body.onclick = function(event){
    console.log("Body clicked");
};复制代码

对于这个例子而言,若是不调用 stopPropagation(),就会在单击按钮时出现两个警告框。但是,因为 click 事件根本不会传播到 document.body,所以就不会触发注册在这个元素上的 onclick 事件处理程序。

事件对象的 eventPhase 属性,能够用来肯定事件当前正位于事件流的哪一个阶段。若是是在捕获阶段调用的事件处理程序,那么 eventPhase 等于 1;若是事件处理程序处于目标对象上,则 eventPhase 等于 2;若是是在冒泡阶段调用的事件处理程序,eventPhase 等于 3。这里要注意的是,尽管“处于目标”发生在冒泡阶段,但 eventPhase 仍然一直等于 2。来看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log(event.eventPhase); // 2
};
document.body.addEventListener("click", function(event){
    console.log(event.eventPhase); // 1
}, true);
document.body.onclick = function(event){
    console.log(event.eventPhase); // 3
};复制代码

当单击这个例子中的按钮时,首先执行的事件处理程序是在捕获阶段触发的添加到 document.body 中的那一个,结果会弹出一个警告框显示表示 eventPhase1。接着,会触发在按钮上注册的事件处理程序,此时的 eventPhase 值为 2。最后一个被触发的事件处理程序,是在冒泡阶段执行的添加到 document.body 上的那一个,显示 eventPhase 的值为 3。而当 eventPhase 等于 2 时,thistargetcurrentTarget 始终都是相等的。

只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就会被销毁。

IE 中的事件对象

与访问 DOM 中的 event 对象不一样,要访问IE中的 event 对象有几种不一样的方式,取决于指定事件处理程序的方法。在使用 DOM1 级方法添加事件处理程序时,event 对象做为 window 对象的一个属性存在。来看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    var event = window.event;
    console.log(event.type);     // "click"
};复制代码

在此,咱们经过 window.event 取得了 event 对象,并检测了被触发事件的类型(IE 中的 type 属性与 DOM 中的 type 属性是相同的)。但是,若是事件处理程序是使用 attachEvent() 添加的,那么就会有一个 event 对象做为参数被传入事件处理程序函数中,以下所示。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
    console.log(event.type);     // "click"
});复制代码

在像这样使用 attachEvent() 的状况下,也能够经过 window 对象来访问 event 对象,就像使用 DOM1 级方法时同样。不过为方便起见,同一个对象也会做为参数传递。

若是是经过 HTML 特性指定的事件处理程序,那么还能够经过一个名叫 event 的变量来访问 event 对象(与 DOM 中的事件模型相同)。再看一个例子。

<input type="button" value="Click Me" onclick="console.log(event.type)">复制代码

IE 的 event 对象一样也包含与建立它的事件相关的属性和方法。其中不少属性和方法都有对应的或者相关的 DOM 属性和方法。与 DOM 的 event 对象同样,这些属性和方法也会由于事件类型的不一样而不一样,但全部事件对象都会包含下表所列的属性和方法。

  • cancelBubble,默认值为 false,但将其设置为 true 就能够取消事件冒泡(与 DOM 中的 stopPropagation() 方法的做用相同)。
  • returnValue,默认值为 true,但将其设置为 false 就能够取消事件的默认行为(与 DOM 中的 preventDefault() 方法的做用相同) 。
  • srcElement,事件的目标(与 DOM 中的 target 属性相同) 。
  • type,被触发的事件的类型 。

由于事件处理程序的做用域是根据指定它的方式来肯定的,因此不能认为 this 会始终等于事件目标。故而,最好仍是使用 event.srcElement 比较保险。例如:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log(window.event.srcElement === this);  // true
};
btn.attachEvent("onclick", function(event){
    console.log(event.srcElement === this);         // false
});复制代码

在第一个事件处理程序中(使用 DOM1 级方法指定的),srcElement 属性等于 this,但在第二个事件处理程序中,这二者的值不相同。

如前所述,returnValue 属性至关于 DOM 中的 preventDefault() 方法,它们的做用都是取消给定事件的默认行为。只要将 returnValue 设置为 false,就能够阻止默认行为。来看下面的例子。

var link = document.getElementById("myLink");
link.onclick = function(){
    window.event.returnValue = false;
};复制代码

这个例子在 onclick 事件处理程序中使用 returnValue 达到了阻止连接默认行为的目的。与 DOM 不一样的是,在此没有办法肯定事件是否能被取消。

相应地,cancelBubble 属性与 DOM 中的 stopPropagation() 方法做用相同,都是用来中止事件冒泡的。因为IE不支持事件捕获,于是只能取消事件冒泡;但 stopPropagatioin() 能够同时取消事件捕获和冒泡。例如:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    console.log("Clicked");
    window.event.cancelBubble = true;
};
document.body.onclick = function(){
    console.log("Body clicked");
};复制代码

经过在 onclick 事件处理程序中将 cancelBubble 设置为 true,就可阻止事件经过冒泡而触发 document.body 中注册的事件处理程序。结果,在单击按钮以后,只会显示一个警告框。

跨浏览器的事件对象

虽然 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;
        }
    }
};复制代码

以上代码显示,咱们为 EventUtil 添加了4个新方法。第一个是 getEvent(),它返回对 event 对象的引用。考虑到 IE 中事件对象的位置不一样,可使用这个方法来取得 event 对象,而没必要担忧指定事件处理程序的方式。在使用这个方法时,必须假设有一个事件对象传入到事件处理程序中,并且要把该变量传给这个方法,以下所示。

btn.onclick = function(event){
    event = EventUtil.getEvent(event);
};复制代码

在兼容 DOM 的浏览器中,event 变量只是简单地传入和返回。而在 IE 中,event 参数是未定义的 undefined,所以就会返回 window.event。将这一行代码添加到事件处理程序的开头,就能够确保随时都能使用 event 对象,而没必要担忧用户使用的是什么浏览器。

第二个方法是 getTarget(),它返回事件的目标。在这个方法内部,会检测 event 对象的 target 属性,若是存在则返回该属性的值;不然,返回 srcElement 属性的值。能够像下面这样使用这个方法。

btn.onclick = function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
};复制代码

第三个方法是 preventDefault(),用于取消事件的默认行为。在传入 event 对象后,这个方法会检查是否存在 preventDefault() 方法,若是存在则调用该方法。若是 preventDefault() 方法不存在,则将 returnValue 设置为 false。下面是使用这个方法的例子。

var link = document.getElementById("myLink");
link.onclick = function(event){
    event = EventUtil.getEvent(event);
    EventUtil.preventDefault(event);
};复制代码

以上代码能够确保在全部浏览器中单击该连接都不会打开另外一个页面。首先,使用 EventUtil.getEvent() 取得 event 对象,而后将其传入到 EventUtil.preventDefault() 以取消默认行为。

第四个方法是 stopPropagation(),其实现方式相似。首先尝试使用DOM方法阻止事件流,不然就使用 cancelBubble 属性。下面看一个例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    console.log("Clicked");
    event = EventUtil.getEvent(event);
    EventUtil.stopPropagation(event);
};
document.body.onclick = function(event){
    console.log("Body clicked");
};复制代码

在此,首先使用 EventUtil.getEvent() 取得了 event 对象,而后又将其传入到 EventUtil.stopPropagation()。别忘了因为 IE 不支持事件捕获,所以这个方法在跨浏览器的状况下,也只能用来阻止事件冒泡。

事件类型

Web 浏览器中可能发生的事件有不少类型。如前所述,不一样的事件类型具备不一样的信息,而 DOM3 级事件规定了如下几类事件。

  • UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
  • 焦点事件,当元素得到或失去焦点时触发;
  • 鼠标事件,当用户经过鼠标在页面上执行操做时触发;
  • 滚轮事件,当使用鼠标滚轮(或相似设备)时触发;
  • 文本事件,当在文档中输入文本时触发;
  • 键盘事件,当用户经过键盘在页面上执行操做时触发;
  • 合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发;
  • 变更(mutation)事件,当底层 DOM 结构发生变化时触发。
  • 变更名称事件,当元素或属性名变更时触发。此类事件已经被废弃,没有任何浏览器实现它们,所以本章不作介绍。

除了这几类事件以外,HTML5 也定义了一组事件,而有些浏览器还会在 DOM 和 BOM 中实现其余专有事件。这些专有的事件通常都是根据开发人员需求定制的,没有什么规范,所以不一样浏览器的实现有可能不一致。

DOM3 级事件模块在 DOM2 级事件模块基础上从新定义了这些事件,也添加了一些新事件。包括 IE9 在内的全部主流浏览器都支持 DOM2 级事件。 IE9 也支持 DOM3 级事件。

想要了解更多 DOM 和 HTML5 事件,请参见最新版的 W3C 规范:
www.w3.org/TR/uievents…

小结

事件是将 JavaScript 与网页联系在一块儿的主要方式。DOM3 级事件规范和 HTML5 定义了常见的大多数事件。即便有规范定义了基本事件,但不少浏览器仍然在规范以外实现了本身的专有事件,从而为开发人员提供更多掌握用户交互的手段。有些专有事件与特定设备关联,例如移动 Safari 中的 orientationchange 事件就是特定关联 iOS 设备的。

在使用事件时,须要考虑以下一些内存与性能方面的问题。

  • 有必要限制一个页面中事件处理程序的数量,数量太多会致使占用大量内存,并且也会让用户感受页面反应不够灵敏。
  • 创建在事件冒泡机制之上的事件委托技术,能够有效地减小事件处理程序的数量。
  • 建议在浏览器卸载页面以前移除页面中的全部事件处理程序。

可使用 JavaScript 在浏览器中模拟事件。DOM2 级事件和 DOM3 级事件规范规定了模拟事件的方法,为模拟各类有定义的事件提供了方便。此外,经过组合使用一些技术,还能够在某种程度上模拟键盘事件。IE8 及以前版本一样支持事件模拟,只不过模拟的过程有些差别。

关卡

凭理解和记忆手写 EventUtil 通用类。

var EventUtil = {
    addHandler: function(element, type, handler){
        // 待补充的代码
    },
    removeHandler: function(element, type, handler){
        // 待补充的代码
    },
    getEvent: function(event){
        // 待补充的代码
    },
    getTarget: function(event){
        // 待补充的代码
    },
    preventDefault: function(event){
        // 待补充的代码
    },
    stopPropagation: function(event){
        // 待补充的代码
    }
};复制代码

更多

关注微信公众号「劼哥舍」回复「答案」,获取关卡详解。
关注 github.com/stone0090/j…,获取最新动态。

相关文章
相关标签/搜索