事件

JavaScriptHTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式的模型,支持页面的行为(JavaScript代码)与页面的外观(HTMLCSS代码)之间的松散耦合。

事件流

事件流描述的是从页面中接收事件的顺序。但有意思的是, IENetscape 开发团队居然提出了差不多是完全相反的事件流的概念。 IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。

  • IE 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那节点)接收,然后逐级向上传播到较为不具体的节点(文档)。



    Event Bubbling Example


    
Click Me

click 点击的顺序:div => body => html => document

  • 事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。



    Event Bubbling Example


    
Click Me

click 点击的顺序: document => html => body => div

DOM事件流

DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

IE9OperaFirefoxChromeSafari都支持 DOM事件流;IE8及更早版本不支持DOM 事件流。

事件处理程序

事件就是用户和浏览器自身执行的某种动作,事件处理程序就是响应某个事件的函数就叫做事件处理函数,事件处理程序以on 开头。click 事件的事件处理程序就是 onclickload事件的事件处理程序就是onload

HTML 事件处理程序


当单击这个按钮时,就会显示一个警告框。这个操作是通过指定 onclick 特性并将一些 JavaScript代码作为它的值来定义的。

Dom0 及事件处理程序

通过 JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。

每个元素(包括 windowdocument)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:

var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert("Clicked");
};

使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的 this 引用当前元素。来看一个例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(this.id); //"myBtn"
};
  • 以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

  • 也可以删除通过DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可:

btn.onclick = null; //删除事件处理程序

将事件处理程序设置为null之后,再单击按钮将不会有任何动作发生。

DOM2 级事件处理程序

DOM2 级事件 定义了两个方法,用于处理指定和删除事件处理程序的操作: addEventListener()removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。

最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
    alert(this.id);
}, false);

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

btn.addEventListener("click", function(){
    alert("Hello world!");
}, false);

因为在 addEventListener()removeEventListener()中使
用了相同的函数。可以考虑将事件处理函数单独抽取出来。

var btn = document.getElementById("myBtn");
var handler = function(){
    alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!

IE9FirefoxSafariChromeOpera支持 DOM2 级事件处理程序。

IE事件处理程序

IE 实现了与DOM中类似的两个方法: attachEvent()detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。

由于IE8及更早版本只支持事件冒泡,所以通过 attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
    alert("Clicked");
});

注意,attachEvent()的第一个参数是onclick,而非DOMaddEventListener()方法中的click

IE中使用 attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用 DOM0 级方法的情况下,事件处理程序会在其所属元素的作用域内运行;

在使用 attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this 等于window

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
    alert(this === window); //true
});

不过,与 DOM方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
    alert("Clicked");
});
btn.attachEvent("onclick", function(){
    alert("Hello world!");
});

这里调用了两次 attachEvent(),为同一个按钮添加了两个不同的事件处理程序。单击这个例子中的按钮,首先看到的是"Hello world!",然后才是"Clicked"

跨浏览器的事件处理程序

// 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;
      }
    }
  };
var btn = document.getElementById("myBtn");
var handler = function(){
    alert("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
//这里省略了其他代码
EventUtil.removeHandler(btn, "click", handler);

这两个方法首先都会检测传入的元素中是否存在DOM2级方法。如果存在 DOM2 级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false(表示冒泡阶段)。

如果存在的是IE 的方法,则采取第二种方案。注意,为了在 IE8及更早版本中运行,此时的事件类型必须加上"on"前缀。

最后一种可能就是使用DOM0级方法(在现代浏览器中,应该不会执行这里的代码)。此时,我们使用的是方括号语法来将属性名指定为事件处理程序,或者将属性设置为 null。

事件对象

在触发 DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。

DOM中的事件对象

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

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert(event.type); //"click"
};
btn.addEventListener("click", function(event){
    alert(event.type); //"click"
}, false);
属性/方法 类 型 读/写 说 明
bubbles Boolean 只读 表明事件是否冒泡
cancelable Boolean 只读 表明是否可以取消事件的默认行为
currentTarget Element 只读 其事件处理程序当前正在处理事件的那个元素
defaultPrevented Boolean 只读 true 表 示 已 经 调 用 了 preventDefault()DOM3级事件中新增)
detail Integer 只读 与事件相关的细节信息
eventPhase Integer 只读 调用事件处理程序的阶段: 1 表示捕获阶段, 2 表示“处于目标”, 3 表示冒泡阶段
preventDefault() Function 只读 取消事件的默认行为 。 如 果 cancelabletrue,则可以使用这个方法
stopImmediatePropagation() Function 只读 取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(DOM3级事件中新增)
stopPropagation() Function 只读 取消事件的进一步捕获或冒泡。如果bubblestrue,则可以使用这个方法
target Element 只读 事件的目标
trusted Boolean 只读 true表示事件是浏览器生成的。为false表示事件是由开发人员通过 JavaScript 创建的(DOM3级事件中新增)
type String 只读 被触发的事件的类型
view AbstractView 只读 与事件关联的抽象视图。等同于发生事件的 window对象

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

var btn = document.getElementById("myBtn");
  var handler = function(event){
    switch(event.type){
      case "click":
        alert("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属性,让函数能够确定发生了什么事件,并执行相应的操作。

IE 中的事件对象

在使用 DOM0 级方法添加事件处理程序时,event对象作为 window 对象的一个属性存在。来看下面的例子。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //"click"
};

我们通过 window.event取得了 event对象,并检测了被触发事件的类型(IE中的 type 属性与DOM 中的 type 属性是相同的)。

如果事件处理程序是使用 attachEvent()添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中,如下所示。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
    alert(event.type); //"click"
});

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

属性/方法 类 型 读/写 说 明
cancelBubble Boolean 读/写 默认值为false,但将其设置为true就可以取消事件冒泡(与DOM中的stopPropagation()方法的作用相同)
returnValue Boolean 读/写 默认值为true,但将其设置为false就可以取消事件的默认行为(与DOM中的preventDefault()方法的作用相同)
srcElement Element 只读 事件的目标(与DOM中的target属性相同)
type String 只读 被触发的事件的类型
var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(window.event.srcElement === this); //true
};
var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(window.event.srcElement === this); //true
};
btn.attachEvent("onclick", function(event){
    alert(event.srcElement === this); //false
    // this => window
});

returnValue 属性相当于 DOM中的preventDefault()方法,它们的作用都是取消给定事件的默认行为。只要将 returnValue 设置为false,就可以阻止默认行为。来看下面的例子。

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

跨浏览器的事件对象

虽然 DOMIE中的event 对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。IEevent 对象的全部信息和方法 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;
      }
    }
  };

事件类型

UI事件

load:当页面完全加载后在 window上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在元素上面触发,或者当嵌入的内容加载完毕时在元素上面触发。

unload:当页面完全卸载后在window上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在元素上面触发。

abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在元素上面触发。

error:当发生JavaScript 错误时在 window上面触发,当无法加载图像时在元素上面触发,当无法加载嵌入内容时在元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。

select:当用户选择文本框()中的一或多个字符时触发。

resize:当窗口或框架的大小变化时在 window 或框架上面触发。

scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。 元素中包含所加载页面的滚动条。

鼠标与滚轮事件

click:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保易访问性很重要,意味着onclick事件处理程序既可以通过键盘也可以通过鼠标执行。

dblclick:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不是DOM2 级事件规范中规定的,但鉴于它得到了广泛支持,所以 DOM3 级事件将其纳入了标准。

mousedown:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。

mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。 DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它纳入了规范。 IEFirefox 9+Opera支持这个事件。

mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。 DOM2 级事件并没有定义这个事件,但DOM3 级事件将它纳入了规范。IEFirefox 9+Opera支持这个事件。

mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。

mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。

mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。

mouseup:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。

页面上的所有元素都支持鼠标事件。除了 mouseentermouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。

只有在同一个元素上相继触发 mousedownmouseup事件,才会触发 click 事件;如果 mousedownmouseup 中的一个被取消,就不会触发click 事件。类似地,只有触发两次 click 事件,才会触发一次dblclick 事件。如果有代码阻止了连续两次触发 click 事件(可能是直接取消click 事件,也可能通过取消 mousedownmouseup间接实现),那么就不会触发 dblclick事件了。

这 4 个事件触发的顺序始终如下:

(1)mousedown

(2) mouseup

(3)click

(4) mousedown

(5) mouseup

(6)click

(7) dblclick

客户端的坐标位置

鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的 clientXclientY属性中。所有浏览器都支持这两个属性,它们的值表示事件发生时鼠标指针在视口中的水平
和垂直坐标。

页面的坐标位置

通过客户区坐标能够知道鼠标是在视口中什么位置发生的,而页面坐标通过事件对象的pageXpageY属性,能告诉你事件是在页面中的什么位置发生的。换句话说,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。

屏幕坐标位置

鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过 screenXscreenY 属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。

HTML5 事件

很多浏览器出于不同的目的——满足用户需求或解决特殊问题,还实现了一些自定义的事件。HTML5详尽列出了浏览器应该支持的所有事件。

contextmenu 事件

为了实现上下文菜单,开发人员面临的主要问题是如何确定应该显示上下文菜单(在Windows 中,是右键单击;在 Mac中,是 Ctrl+单击),以及如何屏蔽与该操作关联的默认上下文菜单。为解决这个问题,就出现了contextmenu 这个事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。

由于contextmenu 事件是冒泡的,因此可以为document指定一个事件处理程序,用以处理页面中发生的所有此类事件。这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在兼容DOM的浏览器中,使用 event.preventDefalut();在IE中,将 event.returnValue的值设置为false

  EventUtil.addHandler(window, "load", function(event){
    var div = document.getElementById("myDiv");
    EventUtil.addHandler(div, "contextmenu", function(event){
      event = EventUtil.getEvent(event);
      EventUtil.preventDefault(event);
      var menu = document.getElementById("myMenu");
      menu.style.left = event.clientX + "px";
      menu.style.top = event.clientY + "px";
      menu.style.visibility = "visible";
    });
    EventUtil.addHandler(document, "click", function(event){
      document.getElementById("myMenu").style.visibility = "hidden";
    });
  });

DOMContentLoaded 事件

如前所述, windowload事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而颇费周折。而DOMContentLoaded事件则在形成完整的DOM树之后就会触发,不理会图像、 JavaScript 文件、CSS文件或其他资源是否已经下载完毕。

要处理 DOMContentLoaded 事件,可以为 documentwindow添加相应的事件处理程序(尽管这个事件会冒泡到 window,但它的目标实际上是document)。来看下面的例子。

EventUtil.addHandler(document, "DOMContentLoaded", function(event){
    alert("Content loaded");
});

readystatechange 事件

IE 为 DOM 文档中的某些部分提供了 readystatechange 事件。这个事件的目的是提供与文档或元素的加载状态有关的信息,但这个事件的行为有时候也很难预料。支持 readystatechange 事件的每个对象都有一个 readyState 属性,可能包含下列 5 个值中的一个。

uninitialized(未初始化):对象存在但尚未初始化。
loading(正在加载):对象正在加载数据。
loaded(加载完毕):对象加载数据完成。
interactive(交互):可以操作对象了,但还没有完全加载。
complete(完成):对象已经加载完毕。

对于 document 而言,值为"interactive"readyState会在与 DOMContentLoaded 大致相同的时刻触发readystatechange 事件。此时,DOM 树已经加载完毕,可以安全地操作它了,因此就会进入交互(interactive)阶段。但与此同时,图像及其他外部文件不一定可用。

ventUtil.addHandler(document, "readystatechange", function(event){
    if (document.readyState == "interactive"){
        alert("Content loaded");
    }
});

交互阶段可能会早于也可能会晚于完成阶段出现,无法确保顺序。在包含较多外部资源的页面中,交互阶段更有可能早于完成阶段出现;而在页面中包含较少外部资源的情况下,完成阶段先于交互阶段出现的可能性更大。因此,为了尽可能抢到先机,有必要同时检测交互和完成阶段,如下面的例子所示。

EventUtil.addHandler(document, "readystatechange", function(event){
    if (document.readyState == "interactive" || document.readyState == "complete"){
        EventUtil.removeHandler(document, "readystatechange",         arguments.callee);
        alert("Content loaded");
    }
});

支持 readystatechange事件的浏览器有IEFirfox 4+Opera

虽然使用 readystatechange可以十分近似地模拟 DOMContentLoaded事件,但它们本质上还是不同的。在不同页面中,load事件与 readystatechange 事件并不能保证以相同的顺序触发。

内存和性能

由于事件处理程序可以为现代 Web应用程序提供交互能力,因此许多开发人员会不分青红皂白地向页面中添加大量的处理程序。在创建GUI 的语言(如C#)中,为GUI中的每个按钮添加一个 onclick事件处理程序是司空见惯的事,而且这样做也不会导致什么问题。可是在 JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。

首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的
角度出发,还是有一些方法能够提升性能的。

事件委托

如果在一个复杂的Web 应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在DOM 树中尽量最高的层次上添加一个事件处理程序,如下面的例子所示。

var list = document.getElementById("myLinks");
  EventUtil.addHandler(list, "click", function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    switch(target.id){
      case "doSomething":
        document.title = "I changed the document's title";
        break;
      case "goSomewhere":
        location.href = "http://www.wrox.com";
        break;
      case "sayHi":
        alert("hi");
        break;
    }
});

移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成Web应用程序内存与性能问题的主要原因。

如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,如下面的例子所示。

btn.onclick = function(){
    //先执行某些操作
    btn.onclick = null; //移除事件处理程序
    document.getElementById("myDiv").innerHTML = "Processing...";
}

在此,我们在设置

innerHTML 属性之前,先移除了按钮的事件处理程序。这样就确保了内存可以被再次利用,而从 DOM中移除按钮也做到了干净利索。注意,在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。

参考文献

《javascript 高级程序设计3》

你可能感兴趣的:(事件)