第 13 章 事件

79 阅读9分钟

JavaScript 与 HTML 之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。

13.1 事件流

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

13.1.1 事件冒泡

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

<!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 对象。

image.png

13.1.2 事件捕获

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。

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

image.png

13.1.3 DOM事件流

“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

image.png

13.2 事件处理程序

事件就是用户或浏览器自身执行的某种动作。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以"on"开头,因此click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。

13.2.1 HTML事件处理程序

<input type="button" value="Click Me" onclick="alert('Clicked')" />

通过 event 变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。

<!-- 输出 "click" -->
<input type="button" value="Click Me" onclick="alert(event.type)">

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

<!-- 输出 "Click Me" -->
<input type="button" value="Click Me" onclick="alert(this.value)">

通过 HTML 指定事件处理程序的最后一个缺点是 HTML 与 JavaScript 代码紧密耦合。如果要更换事 件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事 件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。

13.2.2 DOM0 级事件处理程序

通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。

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

删除通过 DOM0 级方法指定的事件处理程序

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

13.2.3 DOM2 级事件处理程序

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

  • type: 要处理的事件名
  • listener:作为事件处理程序的函数
  • useCapture:默认值为false,表示捕获;true,表示冒泡;

DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序

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

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

通过 addEventListener()添加的事件处理程序只能使用 removeEventListener()来移除; 传入 removeEventListener()中的事件处理程序函数必须与传入addEventListener()中的相同;

var btn = document.getElementById("myBtn");
var handler = function(){
    alert(this.id);
};

btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!

13.2.4 IE事件处理程序

IE 实现了与 DOM 中类似的两个方法:attachEvent()和 detachEvent()。这两个方法接受相同 的两个参数:事件处理程序名称与事件处理程序函数。通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段

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

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

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

可以用来为一个元素添加多个事件处理程序。

var btn = document.getElementById("myBtn");

btn.attachEvent("onclick", function(){
    alert("Clicked");
});

btn.attachEvent("onclick", function(){
    alert("Hello world!");
});

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

使用 attachEvent()添加的事件可以通过 detachEvent()来移除,条件是必须提供相同的参数。

13.2.5 跨浏览器的事件处理程序

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;
        }
    }
};

13.3 事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与事件有关的 信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。所有浏览器都支持 event 对象,但支持方式不同。

13.3.1 DOM中的事件对象

兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法

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

btn.addEventListener("click", function(event){
    alert(event.type); //"click"
}, false);

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实 际目标。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert(event.currentTarget === this); //true
    alert(event.target === this); //true
};

要阻止特定事件的默认行为,可以使用 preventDefault()方法。

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

stopPropagation()方法用于立即停止事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡。

var btn = document.getElementById("myBtn");

btn.onclick = function(event){
    alert("Clicked");
    event.stopPropagation();
};
// body的事件没有触发
document.body.onclick = function(event){
    alert("Body clicked");
};

事件对象的 eventPhase 属性,可以用来确定事件当前正位于事件流的哪个阶段。

  • eventPhase 等于 1:在捕获阶段调用的事件处理程序;
  • eventPhase 等于 2:事件处理程序处于目标对象上;
  • eventPhase 等于 3:在冒泡阶段调用的事件处理程序;

13.3.2 IE中的事件对象

IE 中的 event 对象有几种不同的方式,取决于指定事件处理程序的方法。

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

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

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

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

在IE中this 会不一定等于事件目标

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

btn.attachEvent("onclick", function(event){
    alert(event.srcElement === this); //false
});

returnValue 属性相当于 DOM 中的 preventDefault()方法,它们的作用都是取消给定事件的默认行为。

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

由于 IE 不支持事件捕获,因而只能取消事件冒泡;

var btn = document.getElementById("myBtn");

btn.onclick = function(){
    alert("Clicked");
    window.event.cancelBubble = true;
};

document.body.onclick = function(){
    alert("Body clicked");
};

13.3.3 跨浏览器的事件对象


var EventUtil = {
    getEvent: function(event){
        return event ? event : window.event;
    },

    getTarget: function(event){
        return event.target || event.srcElement;
    }
}

13.4 事件类型

DOM3级事件 规定了以下几类事件。

  • UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
  • 焦点事件,当元素获得或失去焦点时触发;
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发;
  • 滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
  • 文本事件,当在文档中输入文本时触发;
  • 键盘事件,当用户通过键盘在页面上执行操作时触发;
  • 合成事件,当为 IME(Input Method Editor,输入法编辑器)输入字符时触发;
  • 变动(mutation)事件,当底层 DOM 结构发生变化时触发。

13.5 内存和性能

在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。首先,每个 函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程 序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

13.5.1 事件委托

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>
var list = document.getElementById("myLinks");
list.addEventListener("click", function(event){
    event = event ? event : window.event;
    var target = event.target || event.srcElement;
    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;
    }
 });

13.5.2 移除事件处理程序

每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就 会建立一个连接。这种连接越多,页面执行起来就越慢。所以我们在不需要的时候移除事件处理程序。

谨慎产生空事件处理程序

  • 如果带有事件处理程序的元素被 innerHTML 删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收
  • 在页面被卸载之前没有清理干净事件处理程序,那它们就会滞留在内存中。所以,我们可以只要是通过 onload 事件处理程序添加的东西,最后都要通过 onunload 事件处理程序将它们移除。

13.6.1 DOM中的自定义事件类型

使用 createEvent()方法创建 event 对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。这个字符串可以是下列几字符串之一。

  • UIEvents:一般化的 UI 事件。鼠标事件和键盘事件都继承自 UI 事件。DOM3 级中是 UIEvent。
  • MouseEvents:一般化的鼠标事件。DOM3 级中是 MouseEvent。
  • MutationEvents:一般化的 DOM 变动事件。DOM3 级中是 MutationEvent。
  • HTMLEvents:一般化的 HTML 事件。没有对应的 DOM3 级事件(HTML 事件被分散到其他类别中)。

13.6.2 IE中的事件模拟

document.createEventObject()方法可以在 IE 中创建 event 对象。但与 DOM 方式不同 的是,这个方法不接受参数,结果会返回一个通用的 event 对象。然后,你必须手工为这个对象添加 所有必要的信息(没有方法来辅助完成这一步骤)。最后一步就是在目标上调用 fireEvent()方法,这 个方法接受两个参数:事件处理程序的名称和 event 对象。