JavaScript与HTML的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。可以 使用仅在事件发生时执行的监听器(也叫处理程序)订阅事件。在传统软件工程领域,这个模型叫“观察 者模式”,其能够做到页面行为(在JavaScript中定义)与页面展示(在HTML和CSS中定义)的分离。
浏览器的事件系统非常复杂。即使所有主流浏览器都实现了DOM2 Events,规范也没有涵盖所有的事 件类型。BOM也支持事件,这些事件与DOM事件之间的关系由于长期以来缺乏文档,经常容易被混淆 (HTML5已经致力于明确这些关系)。而DOM3新增的事件API又让这些问题进一步复杂化了。根据具体 的需求不同,使用事件可能会相对简单,也可能会非常复杂。但无论如何,理解其中的核心概念还是最重 要的。
事件流
在第四代Web浏览器(IE4和Netscape Communicator 4)开始开发时,开发团队碰到了一个有意思的问 题:页面哪个部分拥有特定的事件呢?要理解这个问题,可以在一张纸上画几个同心圆。把手指放到圆心 上,则手指不仅是在一个圆圈里,而且是在所有的圆圈里。两家浏览器的开发团队都是以同样的方式看待 浏览器事件的。
当你点击一个按钮时,实际上不光点击了这个按钮,还点击了它的容器以及整个页面。 事件流描述了页面接收事件的顺序。结果非常有意思,IE和Netscape开发团队提出了几乎完全相反的 事件流方案。IE将支持事件冒泡流,而Netscape Communicator将支持事件捕获流。
事件冒泡
IE事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触 发,然后向上传播至没有那么具体的元素(文档)。
<!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
也就是说, div元素,即被点击的元素,最先触发 click 事件。然后, click 事件沿DOM树 一路向上,在经过的每个节点上依次触发,直至到达 document 对象。
事件捕获
Netscape Communicator团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点 应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦 截事件。如果前面的例子使用事件捕获,则点击 div 元素会以下列顺序触发 click 事件
(1) document
(2) html
(3) body
(4) div
在事件捕获中, click 事件首先由 document 元素捕获,然后沿DOM树依次向下传播,直至到达 实际的目标元素 。
事件捕获得到了所有现代浏览器的支持。实际 上,所有浏览器都是从 window 对象开始捕获事件,而DOM2 Events规范规定的是从 document 开始。由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况 下可以使用事件捕获。
DOM事件流
DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生, 为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶 段响应事件。
同样是上面的例子,在DOM事件流中,实际的目标( div元素)在捕获阶段不会接收到事件。这是因为捕获阶段从 document 到 html再到 body 就结束了。下一阶段,即会在 div元素上触发事件的“到达目 标”阶段,通常在事件处理时被认为是冒泡阶段的一部分(稍后讨论)。然后,冒泡阶段开始,事件反向 传播至文档。
事件处理程序
事件意味着用户或浏览器执行的某种动作。比如,单击( click )、加载( load )、鼠标悬念 ( mouseover )。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的 名字以 “on” 开头,因此 click 事件的处理程序叫作 onclick ,而 load 事件的处理程序叫作 onload 。有很多方式可以指定事件处理程序。
HTML事件处理程序
特定元素支持的每个事件都可以使用事件处理程序的名字以HTML属性的形式来指定。此时属性的值 必须是能够执行的JavaScript代码。例如,要在按钮被点击时执行某些JavaScript代码,可以使用以下 HTML属性:
<input type="button" value="Click Me" onclick="console.log('Clicked')"/>
因为属性的值是JavaScript代码,所以不能在未经转义的情况下使用HTML语法字 符,比如和号( & )、双引号( “ )、小于号( < )和大于号( > )。此时,为了避免使用HTML实 体,可以使用单引号代替双引号。如果确实需要使用双引号,则要把代码改成下面这样
<input type="button" value="Click Me" onclick="console.log("Clicked")"/>
在HTML中定义的事件处理程序可以包含精确的动作指令,也可以调用在页面其他地方定义的脚本
<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>
<!-- 输出"click" -->
<input type="button" value="Click Me" onclick="console.log(event.type)">
<!-- 输出"Click Me" -->
<input type="button" value="Click Me" onclick="console.log(this.value)">
DOM0事件处理程序
在JavaScript中指定事件处理程序的传统方式是把一个函数赋值给(DOM元素的)一个事件处理程序 属性。这也是在第四代Web浏览器中开始支持的事件处理程序赋值方法,直到现在所有现代浏览器仍然都 支持此方法,主要原因是简单。要使用JavaScript指定事件处理程序,必须先取得要操作对象的引用。 每个元素(包括 window 和 document )都有通常小写的事件处理程序属性,比如 onclick 。只 要把这个属性赋值为一个函数即可
let btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log(this.id); // "myBtn"
};
btn.onclick = null; // 移除事件处理程序
在事件处理 程序里通过 this 可以访问元素的任何属性和方法。以这种方式添加事件处理程序是注册在事件流的冒 泡阶段的。
DOM2事件处理程序
DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()
和 removeEventListener()
。这两个方法暴露在所有DOM节点上,它们接收3个参数:事件名、事件 处理函数和一个布尔值, true 表示在捕获阶段调用事件处理程序, false (默认值)表示在冒泡阶段 调用事件处理程序。
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
与DOM0方式类似,这个事件处理程序同样在被附加到的元素的作用域中运行。使用DOM2 方式的主要优势是可以为同一个事件添加多个事件处理程序。
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
btn.addEventListener("click", () => {
console.log("Hello world!");
}, false);
这里给按钮添加了两个事件处理程序。多个事件处理程序以添加顺序来触发,因此前面的代码会先打 印元素ID,然后显示消息“Hello world!”。
通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 并传入与 添加时同样的参数来移除。这意味着使用 addEventListener() 添加的匿名函数无法移除,
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
// 其他代码
btn.removeEventListener("click", function() { // 没有效果!
console.log(this.id);
}, false);
这个例子通过 addEventListener() 添加了一个匿名函数作为事件处理程序。然后,又以看起来 相同的参数调用了 removeEventListener() 。但实际上,第二个参数与传给 addEventListener() 的完全不是一回事。传给 removeEventListener() 的事件处理函数必须与 传给 addEventListener() 的是同一个,
let btn = document.getElementById("myBtn");
let handler = function() {
console.log(this.id);
};
btn.addEventListener("click", handler, false);
// 其他代码
btn.removeEventListener("click", handler, false); // 有效果!
IE事件处理程序
IE实现了与DOM类似的方法,即 attachEvent() 和 detachEvent() 。这两个方法接收两个同 样的参数:事件处理程序的名字和事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用 attachEvent()添加的事件处理程序会添加到冒泡阶段。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log("Clicked");
});
注意, attachEvent() 的第一个参数是 “onclick” ,而不是DOM的 addEventListener() 方法的 “click” 。
在IE中使用 attachEvent() 与使用DOM0方式的主要区别是事件处理程序的作用域。使用DOM0 方式时,事件处理程序中的 this 值等于目标元素。而使用 attachEvent() 时,事件处理程序是在全 局作用域中运行的,因此 this 等于 window 。
与使用 addEventListener() 一样,使用 attachEvent() 方法也可以给一个元素添加多个事 件处理程序。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log("Clicked");
});
btn.attachEvent("onclick", function() {
console.log("Hello world!");
});
跨浏览器事件处理程序
为了以跨浏览器兼容的方式处理事件,很多开发者会选择使用一个JavaScript库,其中抽象了不同浏 览器的差异。有些开发者也可能会自己编写代码,以便使用最合适的事件处理手段。自己编写跨浏览器事 件处理代码也很简单,主要依赖能力检测。要确保事件处理代码具有最大兼容性,只需要让代码在冒泡阶 段运行即可。
为此,需要先创建一个 addHandler() 方法。这个方法的任务是根据需要分别使用DOM0方式、 DOM2方式或IE方式来添加事件处理程序。这个方法会在 EventUtil 对象(本章示例使用的对象)上添 加一个方法,以实现跨浏览器事件处理。添加的这个 addHandler() 方法接收3个参数:目标元素、事 件名和事件处理函数。
有了 addHandler() ,还要写一个也接收同样的3个参数的 removeHandler() 。这个方法的任 务是移除之前添加的事件处理程序,不管是通过何种方式添加的,默认为DOM0方式。 以下就是包含这两个方法的 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方式,则使用该方 式。注意这时候必须在事件类型前加上 “on” ,才能保证在IE8及更早版本中有效。最后是使用DOM0方 式(在现代浏览器中不会到这一步)。注意使用DOM0方式时使用了中括号计算属性名,并将事件处理程 序或 null 赋给了这个属性。
可以像下面这样使用 EventUtil 对象:
let btn = document.getElementById("myBtn")
let handler = function() {
console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
// 其他代码
EventUtil.removeHandler(btn, "click", handler);
事件实例对象 event
在DOM中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。这个对象包含 了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。例如,鼠标操作导致的事件会生成鼠标位置信息,而键盘操作导致的事件会生成与被按下的键有关的信息。 所有浏览器都支持这个 event 对象,尽管支持方式不同。
DOM事件对象
在DOM合规的浏览器中, event 对象是传给事件处理程序的唯一参数。不管以哪种方式(DOM0或 DOM2)指定事件处理程序,都会传入这个 event 对象。
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.type); // "click"
};
btn.addEventListener("click", (event) => {
console.log(event.type); // "click"
}, false);
在通过HTML属性指定的事件处理程序中,同样可以使用变量 event 引用事件对象。
event 事件对象的公共属性和方法
事件对象包含与特定事件相关的属性和方法。不同的事件生成的事件对象也会包含不同的属性和方法。不过,所有事件对象都会包含下表列出的这些公共属性和方法。
type 触发的事件类型
type 属性在一个处理程序处理多个事件时很有用。
let btn = document.getElementById("myBtn");
let 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种不同的事件: click 、 mouseover 和 mouseout 。
preventDefault()
preventDefault() 方法用于阻止特定事件的默认动作。
let link = document.getElementById("myLink");
link.onclick = function(event) {
event.preventDefault();//阻止链接的默认导航行为
};
stopPropagation()
如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation
方法。
// 事件传播到 p 元素后,就不再向下传播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素后,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
IE事件对象
与DOM事件对象不同, IE事件对象可以基于事件处理程序被指定的方式以不同方式来访问。
DOM0方式指定的事件处理程序
如果事 件处理程序是使用DOM0方式指定的,则 event 对象只是 window 对象的一个属性
var btn = document.getElementById("myBtn");
btn.onclick = function() {
let event = window.event;
console.log(event.type); // "click"
};
attachEvent()(IE事件处理)方式指定的事件处理程序
如果事件处理程序是使用 attachEvent() 指定的,则 event 对象会作为唯一的参数传给处理函数
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event) {
console.log(event.type); // "click"
});
使用HTML属性方式指定的事件处理程序
如果是使用HTML属性方式指定的事件处理程序,则 event 对象同样可以通过变量 event 访问 (与DOM模型一样)。
所有IE事件对象都会包含下表所列的公共属性和方法
事件类型
Web浏览器中可以发生很多种事件。如前所述,所发生事件的类型决定了事件对象中会保存什么信 息。DOM3 Events定义了如下事件类型。