JavaScript 与 HTML 间通过事件实现交互。
事件——文档或浏览器窗口中发生的一些特定的交互瞬间,即用户或浏览器自身执行的某种动作。
可以对事件进行预定,以便事件发生时执行相应的代码。
1. 事件流
事件流描述的是从页面中接收事件的顺序。
1.1 事件冒泡——事件开始时由最具体的元素(文档中嵌套层次最深的节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
单击<div>后,这个 click 事件传播顺序如下:
<div> => <body> => <html> => document
即单击事件在每一级节点上都会发生,直至传播到 document 对象。
1.2 事件捕获——不太具体的节点应更早接收到事件,与事件冒泡顺序相反。
1.3 DOM 事件流
‘DOM2级事件’规定的事件流包括三个阶段:事件捕获 =>处于目标 => 事件冒泡
注:IE9、Opera、Firefox、Chrome 和Safari 都支持DOM 事件流;IE8 及更早版本不支持DOM 事件流。
即使“DOM2 级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox 和Opera 9.5 及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。
2. 事件处理程序
事件——用户或浏览器自身执行的某种动作,如 click、load、mouseover等,而响应某个事件的函数就叫做事件处理程序。事件处理程序以'on'开头,则上述事件对应的事件处理程序为:onclick、onload、onmouseover。为事件指定处理程序的方式有好几种。
2.1 HTML 事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML 特性来指定。这个特性的值应该是能够执行的JavaScript 代码(一段js代码、或调用其他地方定义的函数)。由于这个值是JavaScript,因此不能在其中使用未经转义的HTML 语法字符,例如和号(&)、双引号("")、小于号(<)或大于号(>)。
例: 要在按钮被单击时显示一个警告框,可以像下面这样编写代码:
<input type="button" value="Click Me" onclick="alert(event.value)" /> //输出 "Click Me"
这样指定事件处理程序,会创建一个封装这元素属性值的函数,这个函数中有一个局部变量 event——事件对象。
通过 event 变量,可以直接访问事件对象,不用自己定义,也不用从函数的参数列表中读取。在这个函数内部,this 值等于事件的目标元素。
HTML 事件处理程序缺点:
1)HTML 与 JavaScript 代码紧密耦合,不利于代码维护
2)存在时差问题,事件触发时函数不一定解析完
2.2 DOM0 级事件处理程序
通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。this 指向当前元素。
以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
var btn = document.getElementById("myBtn");
//添加事件处理程序
btn.onclick = function(){
alert(this.id); //myBtn
};
//移除事件处理程序
btn.onclick = null;
2.3 DOM2 级事件处理程序
“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。this 指向当前元素。
优点:可以添加多个事件处理程序,事件处理程序会按照添加他们的顺序触发。
//添加事件处理程序
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
btn.addEventListener("click", function(){
alert("Hello world!");
}, false);
移除事件处理程序 通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除
无效例子:
btn.removeEventListener("click", function(){ //无效!
alert(this.id);
}, false);
有效例子:
var handler = function(){
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,不建议在事件捕获阶段注册事件处理程序。
3. 事件对象
虽然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;
}
}
};
4. 事件类型
DOM3级事件 规定了以下几类事件:
UI(User Interface,用户界面)事件,当用户与页面上的元素交互时触发;
焦点事件,当元素获得或失去焦点时触发;
鼠标事件,当用户通过鼠标在页面上执行操作时触发;
滚轮事件,当使用鼠标滚轮(或类似设备)时触发;
文本事件,当在文档中输入文本时触发;
键盘事件,当用户通过键盘在页面上执行操作时触发;
合成事件,当为IME(Input Method Editor,输入法编辑器)输入字符时触发;
变动(mutation)事件,当底层DOM 结构发生变化时触发。
4.1 UI事件
1)load 事件
当页面完全加载后(包括所有图像、JavaScript 文件、CSS 文件等外部资源),就会触发window 上面的load 事件,我们可以在 window 或 <body> 元素上绑定 load 事件;
EventUtil.addHandler(window, "load", function(event){
alert("Loaded!");
});
或
<body onload="alert('Loaded!')">
当图像加载完成后,会触发 img 上的 load 事件,可以在 <img> 元素上绑定 load 事件;
var image = document.getElementById("myImage");
EventUtil.addHandler(image, "load", function(event){
event = EventUtil.getEvent(event);
alert(EventUtil.getTarget(event).src);
});
或
<img src="smile.gif" onload="alert('Image loaded.')">
在IE9+、Firefox、Opera、Chrome 和Safari 3+及更高版本中,<script>元素也会触发load 事件; IE 和Opera 还支持<link>元素上的load 事件
2)unload 事件
unload 事件与 load 事件对应,在文档被完全卸载后触发。
只要用户从一个页面切换到另一个页面,就会发生 unload 事件,利用这个事件可清除引用,以避免内存泄漏。
与 load 相同,可通过 JavaScript 或微元素添加属性来指定 onunload 事件处理程序。
3)resize 事件
当浏览器窗口的宽度或高度变化时,会触发 resize 事件。这个事件在 window 上触发,因此也可以在 <body> 上来指定事件处理程序。
EventUtil.addHandler(window, "resize", function(event){
alert("Resized");
});
Firefox 只会在停止调整窗口大小时触发 resize 事件,其他浏览器则是变化 1 像素就触发一次。
4)scroll 事件
元素滚动条滚动时触发。
虽然scroll 事件是在window 对象上发生的,但它实际表示的则是页面中相应元素的变化。在混杂模式下,可以通过<body>元素的scrollLeft 和scrollTop 来监控到这一变化;而在标准模式下,除Safari 之外的所有浏览器都会通过<html>元素来反映这一变化(Safari 仍然基于<body>跟踪滚动位置),如下面的例子所示:
EventUtil.addHandler(window, "scroll", function(event){
if (document.compatMode == "CSS1Compat"){
alert(document.documentElement.scrollTop);
} else {
alert(document.body.scrollTop);
}
});
与resize 事件类似,scroll 事件也会在文档被滚动期间重复被触发,所以有必要尽量保持事件处理程序的代码简单。
4.2 焦点事件
焦点事件会在页面元素获得或失去焦点时触发。利用这些事件并与document.hasFocus()方法及 document.activeElement 属性配合,可以知晓用户在页面上的行踪。
常用焦点事件:
blur:元素失去焦点时触发。这个事件不会冒泡,所有浏览器都支持它。
focus:元素获得焦点时触发。这个事件不会冒泡,所有浏览器都支持它。
focusin 与 focus 等价,会冒泡;focusout 与 blur 等价;支持 focusin 和 focusout 的浏览器有 IE5.5+、Safari 5.1+、Opera 11.5+和Chrome。
4.3 鼠标和滚轮事件
鼠标事件:
click:单机鼠标左键或按下回车触发
dblclick:双击鼠标左键触发
mousedown:按下任意鼠标按钮时触发,不能通过键盘触发
mouseenter:鼠标光标从元素外部首次移动到元素范围内时触发,这个事件不冒泡,且光标移动到后代元素上不会触发。
mouseleave:元素上方的鼠标光标移动到元素之外时触发,不冒泡,且光标移动到后代元素上不会触发。
mousemove:当鼠标指针在元素内部移动时重复地触发。
mouseover:鼠标光标从元素外部首次移动到元素范围内时触发,包括被选元素和其子元素。
mouseout:鼠标指针从一个元素上方移出时触发,移出其后代元素上也会触发。
mouseup:释放鼠标按钮式触发。
页面上的所有元素都支持鼠标事件。除了mouseenter 和mouseleave,所有鼠标事件都会冒泡,也可以被取消。
只有在同一个元素上相继触发mousedown 和mouseup 事件,才会触发click 事件;
只有触发两次click 事件,才会触发一次dblclick 事件。
这4个事件触发的顺序始终如下:
(1) mousedown
(2) mouseup
(3) click
(4) mousedown
(5) mouseup
(6) click
(7) dblclick
显然,click 和dblclick 事件都会依赖于其他先行事件的触发;而mousedown 和mouseup 则不受其他事件的影响。
event 属性
1.坐标位置
1)客户区坐标位置:clientX、clientY
参照点为浏览器可视内容区的左上角。即滚动条上下滚动后,同一点的 clientY 会发生变化。
2)页面坐标位置:pageX、pageY
参照点为浏览器内容区左上角。即滚动条上下滚动后,同一点的 clientY 不会发生变化。
3)屏幕坐标位置:screenX、screenY
参照点为电脑屏幕的左上角。获取的是鼠标事件发生时,指针相对于整个屏幕的坐标信息。- 修改键
虽然鼠标事件主要是使用鼠标来触发的,但在按下鼠标时键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键就是Shift、Ctrl、Alt 和Meta(在Windows 键盘中是Windows 键,在苹果机中是Cmd 键),DOM 为此规定了4 个属性,表示这些修改键的状态:shiftKey、ctrlKey、altKey 和metaKey。这些属性中包含的都是布尔值,按下为true,否则为 false。
- 修改键
3.相关元素 —— event.relatedTarget
<body>
<div id="myDiv" style="background-color:red;height:100px;width:100px;"></div>
</body>
如上,当鼠标从 div 上移出,则在 div 上触发 mouseout 事件,相关元素就是 body;body 上触发 mouseover 事件,则相关元素就是 div 。
IE8及之前版本不支持 relatedTarget 属性,但。在mouseover 事件触发时,IE 的fromElement 属性中保存了相关元素;在mouseout 事件触发时,IE 的toElement 属性中保存着相关元素。
可以把下面这个跨浏览器取得相关元素的方法添加到EventUtil 对象中。
var EventUtil = {
//省略了其他代码
getRelatedTarget: function(event){
if (event.relatedTarget){
return event.relatedTarget;
} else if (event.toElement){
return event.toElement;
} else if (event.fromElement){
return event.fromElement;
} else {
return null;
}
},
//省略了其他代码
};
- 4.鼠标按键 —— event.button
click 事件是只有在主鼠标按钮被单击(或按下键盘回车)时才会触发。
但 mousedown 和 mouseup ,存在 event.button,当 event.button 属性有三个值,:0 表示主鼠标按钮(通常为左键),1 表示中间的鼠标按钮(鼠标滚轮按钮),2 表示次鼠标按钮(通常为右键)。
IE8 及之前版本也提供了button 属性,但这个属性的值与DOM 的button 属性有很大差异。所以可以再为EventUtil 对象添加如下 getButton() 方法。
var EventUtil = {
//省略了其他代码
getButton: function(event){
if (document.implementation.hasFeature("MouseEvents", "2.0")){
return event.button;
} else {
switch(event.button){
case 0:
case 1:
case 3:
case 5:
case 7:
return 0;
case 2:
case 6:
return 2;
case 4:
return 1;
}
}
},
//省略了其他代码
};
鼠标滚轮事件 —— mousewheel 事件
这个事件可以再任何元素上触发,最终冒泡到 document 或 window 对象。
mousewheel 的 event 对象包含一个特殊的 wheelDelta 属性,向前滚动鼠标滚轮时,wheelDelta 是120的倍数,向后滚动时,是-120的倍数;Opera9.5 的正负号正好相反。
更多的事件信息——用处不大,只有 IE 支持。
detail:值从0开始,每单击一次,数值加一;
altLeft:按下 Alt 键,则值为 true,altKey 的值也为 true;
ctrlLeft:按下 Ctrl 键,则值为 true,ctrlKey 的值也为 true;
shiftLeft:按下 Shift 键,则值为 true,shiftKey 的值也为 true;
offsetX:光标相对于目标元素边界的x 坐标。
offsetY:光标相对于目标元素边界的y 坐标。
4.4 键盘和文本事件
keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。
keypress:当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。按下Esc 键也会触发这个事件。Safari 3.1 之前的版本也会在用户按下非字符键时触发keypress事件。
keyup:当用户释放键盘上的键时触发
在发生keydown 和keyup 事件时,event 对象的keyCode 属性中会包含一个代码,与键盘上一个特定的键对应。对数字字母字符键,keyCode 属性的值与ASCII 码中对应小写字母或数字的编码相同。
4.5 内存和性能
在JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。
首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。
事实上,从如何利用好事件处理程序的角度出发,还是有一些方法能够提升性能的。
4.5.1事件委托 —— 解决事件处理程序过多
利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
按传统做法,要给三个 li 都添加点击事件,需要逐个添加事件处理程序,利用事件委托,只需要再 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"](http://www.wrox.com%22/);;
break;
case "sayHi":
alert("hi");
break;
}
});
虽然对用户来说最终的结果相同,但这种技术需要占用的内存更少。所有用到按钮的事件(多数鼠标事件和键盘事件)都适合采用事件委托技术。
4.5.2 移除事件处理程序
每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript 代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。
如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。
当通过 inner HTML替换页面中的某一部分的时候,那么这被替换掉的一部分原来绑定的事件处理程序,极有可能无法被当作垃圾回收。则应当在被替换掉之前,先手动移除事件处理程序。
<div id="myDiv">
<input type="button" value="Click Me" id="myBtn">
</div>
<script type="text/javascript">
var btn = document.getElementById("myBtn");
btn.onclick = function(){
//先执行某些操作
btn.onclick = null; //移除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing...";
};
</script>
这样,就确保了内存可以被再次利用。而且,在事件处理程序中删除按钮,也能阻止事件冒泡。
采用事件委托,即将事件处理程序指定给较高层次的元素,也能够解决这一问题。
4.6模拟事件
4.6.1 DOM 中的事件模拟
1)模拟鼠标事件:
createEvent() => initMouseEvent() => dispatchEvent()
模拟对按钮的单击事件
var btn = document.getElementById("myBtn");
//创建事件对象
var event = document.createEvent("MouseEvents");
//初始化事件对象
event.initMouseEvent("click", true, true, document.defaultView, 0, 0, 0, 0, 0,
false, false, false, false, 0, null);
//触发事件
btn.dispatchEvent(event);
在兼容DOM的浏览器中,也可以通过相同的方式来模拟其他鼠标事件(例如dblclick)。
createEvent() 的参数是要创建的事件类型的字符串。在DOM2 级中,所有这些字符串都使用英文复数形式,而在DOM3级中都变成了单数。这个字符串可以是下列几字符串之一。
UIEvents:一般化的UI 事件。鼠标事件和键盘事件都继承自UI 事件。DOM3 级中是UIEvent。
MouseEvents:一般化的鼠标事件。DOM3 级中是MouseEvent。
MutationEvents:一般化的DOM 变动事件。DOM3 级中是MutationEvent。
HTMLEvents:一般化的HTML 事件。没有对应的DOM3 级事件(HTML 事件被分散到其他类别中)。
要注意的是,“DOM2 级事件”并没有专门规定键盘事件,后来的“DOM3 级事件”中才正式将其作为一种事件给出规定。IE9 是目前唯一支持DOM3 级键盘事件的浏览器。不过,在其他浏览器中,在现有方法的基础上,可以通过几种方式来模拟键盘事件。
2)模拟键盘事件
DOM3 级规定,调用createEvent()并传入"KeyboardEvent"就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法,再把event 对象传给dispatchEvent()方法,即可触发事件。
由于DOM3级不提倡使用keypress 事件,因此只能利用这种技术来模拟keydown 和keyup 事件。
下面例子模拟按住Shift 的同时又按下A 键。
var textbox = document.getElementById("myTextbox"),
event;
//以DOM3 级方式创建事件对象
if (document.implementation.hasFeature("KeyboardEvents", "3.0")){
event = document.createEvent("KeyboardEvent");
//初始化事件对象
event.initKeyboardEvent("keydown", true, true, document.defaultView, "a",0, "Shift", 0);
}
//触发事件
textbox.dispatchEvent(event);
在Firefox 中,调用createEvent()并传入"KeyEvents"就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法,这个方法接收10个参数,将创建的event 对象传入到dispatchEvent()方法就可以触发键盘事件。
在其他浏览器中,则需要创建一个通用的事件,然后再向事件对象中添加键盘事件特有的信息。
4.6.2 IE 中的事件模拟
document.createEventObject() => 手动为 event 添加属性 => fireEvent()
var btn = document.getElementById("myBtn");
//创建事件对象
var event = document.createEventObject();
//初始化事件对象
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);
小结:
在使用事件时,需要考虑如下一些内存与性能方面的问题。
有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。
建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。
建议在浏览器卸载页面之前移除页面中的所有事件处理程序。