目录
1、事件流
事件流描述了页面接收事件的顺序,IE支持事件冒泡流,Netscape Communicator支持事件捕获流。可以这样理解,在一张纸上画几个同心圆,把手指放到圆心上,则手指不仅是在一个圆圈里,而是在所有的圆圈里。当你点击一个按钮时,实际上不光点击了这个按钮,还点击了它的容器以及整个页面。
1.1 事件冒泡
IE事件流被称为事件冒泡,这是因为事件被定义为从具体的元素(文档树最深的节点)开始触发,逐级往上传播。
比如有以下页面:
<! DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
在点击页面中的<div>元素后,click事件会以如下顺序发生:
1.2 事件捕获
事件捕获与事件冒泡顺序相反
由于旧版本浏览器不支持,因此实际当中几乎不会使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。
1.3 DOM事件流
DOM2 Events规范规定事件流分为3个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
在DOM事件流中,实际的目标(div元素)在捕获阶段不会接收到事件。这是因为捕获阶段从document到<html>再到<body>就结束了。下一阶段,会在<div>元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分。然后,冒泡阶段开始,事件反向传播至文档。
大多数支持DOM事件流的浏览器实现了一个小小的拓展。虽然DOM2 Events规范明确捕获阶段不命中事件目标,但现代浏览器都会在捕获阶段在事件目标上触发事件。最终结果是在事件目标上有两个机会来处理事件。
2、事件处理程序
为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以"on"开头,因此click事件的处理程序叫作onclick,而load事件的处理程序叫作onload。有很多方式可以指定事件处理程序。
2.1 HTML事件处理程序
<script>
function showMessage() {
console.log("Hello world! ");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()"/>
以这种方式指定的事件处理程序有一些特殊的地方。首先,会创建一个函数来封装属性的值。这个函数有一个特殊的局部变量event,其中保存的就是event对象
这个动态创建的包装函数还有一个特别有意思的地方,就是其作用域链被扩展了。在这个函数中,document和元素自身的成员都可以被当成局部变量来访问。这是通过使用with实现的:
function() {
with(document) {
with(this) {
// 属性值
}
}
}
这意味着事件处理程序可以更方便地访问自己的属性。
<! -- 输出"Click Me" -->
<input type="button" value="Click Me" onclick="console.log(value)">
如果这个元素是一个表单输入框,则作用域链中还会包含表单元素,事件处理程序对应的函数等价于如下这样:
function () {
with(document) {
with(this.form) {
with(this) {
// 属性值
}
}
}
}
经过这样的扩展,事件处理程序的代码就可以不必引用表单元素,而直接访问同一表单中的其他成员了。下面的例子就展示了这种成员访问模式:
<form method="post">
<input type="text" name="username" value="">
<input type="button" value="Echo Username"
onclick="console.log(username.value)">
</form>
在HTML中指定事件处理程序有一些问题。第一个问题是时机问题。有可能HTML元素已经显示在页面上,用户都与其交互了,而事件处理程序的代码还无法执行。比如在前面的例子中,如果showMessage()函数是在页面后面,在按钮中代码的后面定义的,那么当用户在showMessage()函数被定义之前点击按钮时,就会发生错误。为此,大多数HTML事件处理程序会封装在try/catch块中,以便在这种情况下静默失败,如下面的例子所示:
<input type="button" value="Click Me" onclick="try{showMessage(); }catch(ex) {}">
另一个问题是对事件处理程序作用域链的扩展在不同浏览器中可能导致不同的结果。不同JavaScript引擎中标识符解析的规则存在差异,因此访问无限定的对象成员可能导致错误。
最后一个问题是HTML与JavaScript强耦合,如果需要修改事件处理程序,则必须在两个地方,即HTML和JavaScript中,修改代码。
2.2 DOM0事件处理程序
在JavaScript中指定事件处理程序的传统方式是把一个函数赋值给(DOM元素的)一个事件处理程序属性。要使用JavaScript指定事件处理程序,必须先取得要操作对象的引用。
每个元素(包括window和document)都有通常小写的事件处理程序属性,比如onclick。只要把这个属性赋值为一个函数即可:
let btn = document.getElementById("myButton");
btn.onclick = function () {
console.log("Clicked");
}
注意,前面的代码在运行之后才会给事件处理程序赋值。因此如果在页面中上面的代码出现在按钮之后,则有可能出现用户点击按钮没有反应的情况。
像这样使用DOM0方式为事件处理程序赋值时,所赋函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,即this等于元素。下面的例子演示了使用this引用元素本身
let btn = document.getElementById("myButton");
btn.onclick = function () {
console.log(this.id); // myButton
}
点击按钮,这段代码会显示元素的ID, 不仅仅是id,在事件处理程序里通过this可以访问元素的任何属性和方法。以这种方式添加事件处理程序是注册在事件流的冒泡阶段的。
通过将事件处理程序属性的值设置为null,可以移除通过DOM0方式添加的事件处理程序,如下面的例子所示:
btn.onclick = null; // 移除事件处理程序
2.3 DOM事件处理程序
DOM2 Events为事件处理程序的赋值和移除定义了两个方法:addEventListener()和remove-EventListener()。这两个方法暴露在所有DOM节点上,它们接收3个参数:事件名、事件处理函数和一个布尔值,true表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。
仍以给按钮添加click事件处理程序为例,可以这样写:
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
使用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);
2.4 IE事件处理程序
IE实现了与DOM类似的方法,即attachEvent()和detachEvent()。这两个方法接收两个同样的参数:事件处理程序的名字和事件处理函数。因为IE8及更早版本只支持事件冒泡,所以使用attachEvent()添加的事件处理程序会添加到冒泡阶段。
要使用attachEvent()给按钮添加click事件处理程序,可以使用以下代码:
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。来看下面使用attachEvent()的例子:
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log(this === window); // true
});
与使用addEventListener()一样,使用attachEvent()方法也可以给一个元素添加多个事件处理程序。比如下面的例子:
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
console.log("Clicked");
});
btn.attachEvent("onclick", function() {
console.log("Hello world!");
});
使用attachEvent()添加的事件处理程序将使用detachEvent()来移除,只要提供相同的参数。与使用DOM方法类似,作为事件处理程序添加的匿名函数也无法移除。但只要传给detachEvent()方法相同的函数引用,就可以移除。下面的例子演示了附加和剥离事件:
var btn = document.getElementById("myBtn");
var handler = function() {
console.log("Clicked");
};
btn.attachEvent("onclick", handler);
// 其他代码
btn.detachEvent("onclick", handler);
2.5 跨浏览器事件处理程序
为了以跨浏览器兼容的方式处理事件,很多开发者会选择使用一个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;
}
}
};
这里的addHandler()和removeHandler()方法并没有解决所有跨浏览器一致性问题,比如IE的作用域问题、多个事件处理程序执行顺序问题等。不过,这两个方法已经实现了跨浏览器添加和移除事件处理程序。另外也要注意,DOM0只支持给一个事件添加一个处理程序。好在DOM0浏览器已经很少有人使用了,所以影响应该不大。
3、事件对象
在DOM中发生事件时,所有相关信息都会被收集并存储在一个名为event的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。
3.1 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)
所有事件对象都会包含下表列出的这些公共属性和方法。
3.2 IE事件对象
与DOM事件对象不同,IE事件对象可以基于事件处理程序被指定的方式以不同方式来访问。如果事件处理程序是使用DOM0方式指定的,则event对象只是window对象的一个属性,如下所示:
let btn = document.getElementById("myBtn");
btn.onclick = function () {
let event = window.event;
console.log(event.type); // "click"
};
如果事件处理程序是使用attachEvent()指定的,则event对象会作为唯一的参数传给处理函数,如下所示:
let btn = document.getElementById("myBtn");
btn.attachEvent() = function (event) {
console.log(event.type); // "click"
};
由于事件处理程序的作用域取决于指定它的方式,因此this值并不总是等于事件目标。为此,更好的方式是使用事件对象的srcElement属性代替this。下面的例子表明,不同事件对象上的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
});
在第一个以DOM0方式指定的事件处理程序中,srcElement属性等于this,而在第二个事件处理程序中(运行在全局作用域下),两个值就不相等了。
3.3 跨浏览器事件对象
虽然DOM和IE的事件对象并不相同,但它们有足够的相似性可以实现跨浏览器方案。
var EventUtil = {
addHandler: function(element, type, handler) {
// 为节省版面,删除了之前的代码
},
getEvent: function(event){
returnevent?event: window.event;
},
getTarget: function(event){
returnevent.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;
}
}
};
4. 内存与性能
在JavaScript中,页面中事件处理程序的数量与页面整体直接相关。首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理程序所需访问DOM的次数会先期造成整个页面交互的延迟。只要在使用事件处理程序时多注意一些方法,就可以改善页面性能,接下来详细说明。
4.1事件委托
“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件,只要给所有元素共同的祖先节点添加一个事件处理程序,就可以解决问题。比如:
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
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":
console.log("hi");
break;
}
});
4.2 删除事件处理程序
除了通过事件委托来限制这种连接之外,还应该及时删除不用的事件处理程序。很多Web应用性能不佳都是由于无用的事件处理程序长驻内存导致的。
导致这个问题的原因有两个,第一个是删除带有事件处理程序的元素。比如通过DOM方法removeChild()或replaceChild()删除节点以及innerhtml。这时候 ,删除的元素上如果有事件处理程序,就不会被垃圾收集程序正常清理。
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
let btn = document.getElementById("myBtn");
btn.onclick = function() {
// 执行操作
document.getElementById("myDiv").innerHTML = "Processing...";
// 不好!
};
</script>
这里的按钮在<div>元素中。单击按钮,会将自己删除并替换为一条消息,以阻止双击发生。这是很多网站上常见的做法。问题在于,按钮被删除之后仍然关联着一个事件处理程序。在<div>元素上设置innerHTML会完全删除按钮,但事件处理程序仍然挂在按钮上面。某些浏览器,特别是IE8及更早版本,在这时候就会有问题了。很有可能元素的引用和事件处理程序的引用都会残留在内存中。如果知道某个元素会被删除,那么最好在删除它之前手工删除它的事件处理程序,比如:
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
let btn = document.getElementById("myBtn");
btn.onclick = function() {
// 执行操作
btn.onclick = null; // 删除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing...";
// 不好!
};
</script>
另一个可能导致内存中残留引用的问题是页面卸载。同样,IE8及更早版本在这种情况下有很多问题,不过好像所有浏览器都会受这个问题影响。如果在页面卸载后事件处理程序没有被清理,则它们仍然会残留在内存中。之后,浏览器每次加载和卸载页面(比如通过前进、后退或刷新),内存中残留对象的数量都会增加,这是因为事件处理程序不会被回收。
一般来说,最好在onunload事件处理程序中趁页面尚未卸载先删除所有事件处理程序。
5、模拟事件
事件就是为了表示网页中某个有意义的时刻。通常,事件都是由用户交互或浏览器功能触发。事实上,可能很少有人知道可以通过JavaScript在任何时候触发任意事件,而这些事件会被当成浏览器创建的事件。这意味着同样会有事件冒泡,因而也会触发相应的事件处理程序。这种能力在测试Web应用时特别有用。DOM3规范指明了模拟特定类型事件的方式。IE8及更早版本也有自己模拟事件的方式。
5.1 DOM事件模拟
任何时候,都可以使用document.createEvent()方法创建一个event对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。在DOM2中,所有这些字符串都是英文复数形式,但在DOM3中,又把它们改成了英文单数形式。可用的字符串值是以下值之一。
❑ "UIEvents"(DOM3中是"UIEvent"):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
❑ "MouseEvents"(DOM3中是"MouseEvent"):通用鼠标事件。
❑ "HTMLEvents"(DOM3中没有):通用HTML事件(HTML事件已经分散到了其他事件大类中)。
5.1 模拟鼠标事件
模拟鼠标事件需要先创建一个新的鼠标event对象,然后再使用必要的信息对其进行初始化。要创建鼠标event对象,可以调用createEvent()方法并传入"MouseEvents"参数。这样就会返回一个event对象,这个对象有一个initMouseEvent()方法,用于为新对象指定鼠标的特定信息。initMouseEvent()方法接收15个参数,分别对应鼠标事件会暴露的属性。这些参数列举如下。
❑ type(字符串):要触发的事件类型,如"click"。
❑ bubbles(布尔值):表示事件是否冒泡。为精确模拟鼠标事件,应该设置为true。
❑ cancelable(布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为true。
❑ view(AbstractView):与事件关联的视图。基本上始终是document.defaultView。
❑ detail(整数):关于事件的额外信息。只被事件处理程序使用,通常为0。
❑ screenX(整数):事件相对于屏幕的x坐标。
❑ screenY(整数):事件相对于屏幕的y坐标。
❑ clientX(整数):事件相对于视口的x坐标。
❑ clientY(整数):事件相对于视口的y坐标。
❑ ctrlkey(布尔值):表示是否按下了Ctrl键。默认为false。
❑ altkey(布尔值):表示是否按下了Alt键。默认为false。
❑ shiftkey(布尔值):表示是否按下了Shift键。默认为false。
❑ metakey(布尔值):表示是否按下了Meta键。默认为false。
❑ button(整数):表示按下了哪个按钮。默认为0。
❑ relatedTarget(对象):与事件相关的对象。只在模拟mouseover和mouseout时使用。
显然,initMouseEvent()方法的这些参数与鼠标事件的event对象属性是一一对应的。前4个参数是正确模拟事件唯一重要的几个参数,这是因为它们是浏览器要用的,其他参数则是事件处理程序要用的。event对象的target属性会自动设置为调用dispatchEvent()方法时传入的节点。下面来看一个使用默认值模拟单击事件的例子:
let btn = document.getElementById("myBtn");
// 创建event对象
let event = document.createEvent("MouseEvents");
// 初始化Event对象
event.initMouseEvent("click", true, true, document.defaultView,
0, 0, 0, 0, 0, false, false,
false, false, 0, null);
// 触发事件
btn.dispatchEvent(event);
5.2 自定义DOM事件
DOM3增加了自定义事件的类型。自定义事件不会触发原生DOM事件,但可以让开发者定义自己的事件。要创建自定义事件,需要调用createEvent("CustomEvent")。返回的对象包含initCustomEvent()方法,该方法接收以下4个参数。
❑ type(字符串):要触发的事件类型,如"myevent"。
❑ bubbles(布尔值):表示事件是否冒泡。
❑ cancelable(布尔值):表示事件是否可以取消。
❑ detail(对象):任意值。作为event对象的detail属性。
自定义事件可以像其他事件一样在DOM中派发,比如:
let div = document.getElementById("myDiv"),
event;
div.addEventListener("myevent", (event) => {
console.log("DIV: " + event.detail);
});
document.addEventListener("myevent", (event) => {
console.log("DOCUMENT: " + event.detail);
});
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
event = document.createEvent("CustomEvent");
event.initCustomEvent("myevent", true, false, "Hello world! ");
div.dispatchEvent(event);
}
这个例子创建了一个名为"myevent"的冒泡事件。event对象的detail属性就是一个简单的字符串,<div>元素和document都为这个事件注册了事件处理程序。因为使用initCustomEvent()初始化时将事件指定为可以冒泡,所以浏览器会负责把事件冒泡到document。
5.3 IE事件模拟
在IE8及更早版本中模拟事件的过程与DOM方式类似:创建event对象,指定相应信息,然后使用这个对象触发。当然,IE实现每一步的方式都不一样。
首先,要使用document对象的createEventObject()方法来创建event对象。与DOM不同,这个方法不接收参数,返回一个通用event对象。然后,可以手工给返回的对象指定希望该对象具备的所有属性。(没有初始化方法。)最后一步是在事件目标上调用fireEvent()方法,这个方法接收两个参数:事件处理程序的名字和event对象。调用fireEvent()时,srcElement和type属性会自动指派到event对象(其他所有属性必须手工指定)。这意味着IE支持的所有事件都可以通过相同的方式来模拟。例如,下面的代码在一个按钮上模拟了click事件:
var btn = document.getElementById("myBtn");
// 创建event对象
var event = document.createEventObject();
/// 初始化event对象
event.screenX = 100;
event.screenY = 0;
event.clientX = 0;
event.clientY = 0;
event.ctrlKey = false;
event.altKey = false;
event.shiftKey = false;
event.button = 0;
// 触发事件
btn.fireEvent("onclick", event);
这个例子先创建event对象,然后用相关信息对其进行了初始化。注意,这里可以指定任何属性,包括IE8及更早版本不支持的属性。这些属性的值对于事件来说并不重要,因为只有事件处理程序才会使用它们。