JavaScript 事件总结
- 本文总结自《JavaScript高级程序设计》以及自己平时的经验,针对较新浏览器以及 DOM3 级事件标准(2016年8月),对少部分内容作了更正,增加了各种例子及解析。
- 如无特殊说明,本文后的文字引用和图片引用均来自《JavaScript高级程序设计》,引用稍有改变原文,不改变意思。
- 本文仅作巩固基础之用,如果有不正确的地方,还望指出。
事件流
事件流分为事件冒泡和事件捕获:
- 如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。在浏览器上单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。事件流描述的是从页面中接收事件的顺序。
- IE开发团队提出了事件冒泡流、Netscape开发团队提出了事件捕获流。
事件冒泡
- 事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点,所有现代浏览器都支持事件冒泡,除IE5.5外,均一直冒泡到window。
- 事件冒泡示意图:
事件捕获
- 不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。IE9+、Safari、Chrome、Opera和Firefox支持,且从window开始捕获(尽管DOM2 级事件规范要求从document)。
- 事件捕获示意图:
- 由于老版本的浏览器不支持,因此很少有人使用事件捕获。我们也建议读者放心地使用事件冒泡,在有特殊需要时再使用事件捕获。
- 为了彻底理解事件冒泡和捕获,这里写了个例子:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>test1</title>
<link rel="stylesheet" href="test1.css">
</head>
<body>
<div id="a">
<div id="b">
<div id="c"></div>
</div>
</div>
<script src="test1.js"></script>
</body>
</html>
#a{
width: 300px;
height: 300px;
background: pink;
}
#b{
width: 200px;
height: 200px;
background: blue;
}
#c{
width: 100px;
height: 100px;
background: yellow;
}
var a = document.getElementById("a"),
b = document.getElementById("b"),
c = document.getElementById("c");
c.addEventListener("click", function(event){
console.log("c1")
// 注意第三个参数没有传进 false , 因为默认传进来的是 false,代表冒泡阶段调用,个人认为处于目标阶段也会调用的
});
c.addEventListener("click", function(event){
console.log("c2");
}, true);
b.addEventListener("click", function(event){
console.log("b");
}, true);
a.addEventListener("click", function(event){
console.log("a1");
}, true);
a.addEventListener("click", function(event){
console.log("a2")
});
a.addEventListener("click", function(event){
console.log("a3");
event.stopImmediatePropagation();
}, true);
a.addEventListener("click", function(event){
console.log("a4");
}, true);
-
效果如图
-
点击 c 或 b,输出:a1、a3
-
stopImmediatePropagation 包含了 stopPropagation 的功能,即阻止事件传播(捕获或冒泡),但同时也阻止该元素上后来绑定的事件处理程序被调用,所以不输出 a4,因为事件捕获被拦截了,自然不会触发 b、c 上的事件,所以不输出 b、c1、c2,冒泡更谈不上了,所以不输出 a2。有人会觉得上面的表述有一点点问题,为什么捕获被拦截了,c1 就不输出了呢? c1 应该是冒泡阶段被调用的呀,所以应该改为另一个表述:“...冒泡更谈不上,所以不输出 c1、a2”。但另一个表述是错的,下面会分析到。
-
点击 a,输出 a1、a2、a3
-
不应该是 a1、a3、a2 吗?a1、a3 可是在捕获阶段被调用的处理程序啊,a2 是在冒泡阶段被调用的啊。这里正是要说明的:虽然这三个事件处理程序注册时指定了true、false,但现在事件流是处于目标阶段,不是冒泡阶段、也不是捕获阶段,事件处理程序被调用的顺序是注册的顺序。不论你指定的是 true or false. 这也解释了上面提到的“另一种表述”为什么是错误的。
-
更深一步解释是:要区分事件流和事件处理程序,不论事件处理程序存不存在,事件流都会传播。这是一个观察者模式,绑定事件的节点是被观察者、事件处理程序是观察者,事件流是不知道观察者的存在的,所以你点击页面的时候,事件流一定要传播,传播到某一个节点时,节点去通知所有观察者,也就是调用事件处理程序(有可能观察者不存在)。
-
当一个事件流来到一个节点时,事件流可能在捕获阶段(正在流向最深层次的节点)、可能在处于目标阶段(已经流到了目标,也就是event.target)、也可能在冒泡阶段(正在流向最外层节点)。而事件处理程序是这么处理的:① 注册时第三个参数指定为 true 时,事件流到来,如果事件流是捕获阶段或处于目标阶段,则调用该事件处理程序。②注册时第三个参数指定为 false 时,当事件流到来,如果事件流是处于目标阶段或冒泡阶段,则调用该事件处理程序。
-
所以当事件流是处于目标阶段,那么不管事件处理程序第三个参数指定的true or false,事件处理程序都会被调用,调用顺序按照注册顺序。所以点击a,输出 a1、a2、a3,而不是a1、a3、a2。
-
注释掉 event.stopImmediatePropagation,点击 c,输出 a1、a3、a4、b、c1、c2、a2
-
另外,如果同一个事件处理程序(指针相同,比如用 handler 保存的事件处理程序),用 addEventListener 或 attachEvent 绑定多次,如果第三个参数是相同的话,也只会被调用一次。但如果第三个参数一个设置为true,另一个设置为false,那么会被调用两次。
DOM事件流
- “DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段。(事件处理中“处于目标阶段”被看成冒泡阶段的一部分)。
- IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件,就是有两个机会在目标对象上面操作事件。(尽管DOM2级事件规范明确要求捕获阶段不涉及事件目标)。
事件处理程序
HTML 事件处理程序
简单来讲,HTML 事件处理程序是直接在HTML中绑定事件,如下
<input type="button" value="Click Me" onclick="alert("Clicked")" />
注意事项:
- 不能在其中使用未经转义的HTML语法字符,如
&
、“”
、<
、>
,因为这是在HTML中绑定的,会造成浏览器解析DOM结构错误。 - 扩展函数作用域,来看下面的代码:
<!-- 输出 "Click Me、lzh" -->
<form method="post">
<input type="text" name="username" value="lzh">
<input type="button" value="Click Me" onclick="alert(value);alert(username.value);">
</form>
如果当前元素是一个表单输入元素,浏览器内部大概是这样实现的:
(function () {
with (document) {
with (this.form) {
with (this) {
//元素属性值
}
}
}
})();
如果没有form
元素,调用username
会报错,所以不论是服务端渲染还是Ajax请求回来数据再渲染,最好还是把form结构写完整。
扩展作用域有三个缺点:
- 函数被调用时还没定义会报错,只好
try{}catch(ex){}
,分离的写法可以在DOMContentLoaded之后再绑定。 - 扩展的作用域链在不同浏览器中会导致不同的结果。
- HTML 与 JavaScript 代码紧密耦合,如果要更换事件处理程序,需要改动 HTML 代码和 JavaScript代码。
DOM0级事件处理程序
- 每个元素(包括
window
和document
)都有自己的事件处理程序属性,这些属性通常全部小写。使用 DOM0 级指定的事件处理程序被认为是元素的方法。this
引用当前元素。通过this
可以访问元素的任何属性和方法。DOM0 级事件处理程序在冒泡阶段被处理。
var btn = document.getElementById("myBtn");
btn.onclick = function () {
alert(this.id); //"myBtn"
};
DOM2级事件处理程序
addEventListener()
包含三个参数,要处理的事件名、事件处理函数、布尔值,布尔值为true,表示在捕获阶段调用事件处理程序,反之在冒泡阶段调用。- DOM2 级事件处理程序中的
this
也指向addEventListener
的那个元素。- 可以添加多个事件处理程序,按添加顺序依次调用。
removeEventListener
无法移除匿名函数的事件处理程序。
var btn = document.getElementById("myBtn");
var handler = function () {
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); // 有效!
- IE9、Firefox、Safari、Chrome 和Opera 支持DOM2 级事件处理程序。
IE事件处理程序
attachEvent
detachEvent
接收两个参数,事件处理程序名称、事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以该事件处理程序只支持事件冒泡。- 老版本的Opera支持这种方法,但现在Opera已经改用blink内核,IE11已经不支持这种方法,注意 IE9 就已经支持 DOM2 级事件处理程序了。
- 特别要注意:第一个参数包含on,比如onclick。
- 区别于DOM0 级事件处理程序,
this
指向 'window'。- 也可以添加多个事件处理程序。
跨浏览器的事件处理程序
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;
}
}
};
- 存在问题:
- IE事件处理程序 中的
this
指向window
。- 只支持 DOM0 级的浏览器不能多次添加事件处理程序,不过这种浏览器应该不多了,即使是IE8 也支持attachEvent。
- 会不会有一些事件,在浏览器支持 DOM2 级事件处理程序的情况下,那些事件只能用
on + name
的形式呢? 之前一直怀疑 (1).xhr.onreadystatechange()
和 (2).DOMNodeInserted
事件,这里我多虑了,经过验证,(1).是支持 DOM2 级事件的,(2).天生就是 DOM2 级的。这里只是为了打消我的疑虑,记录下来。
事件对象
DOM 中的事件对象
- 兼容 DOM 的浏览器会将一个
event
对象传入事件处理程序, IE9 及更高版本可以。无论指定事件处理程序时使用什么方法(DOM0 级 DOM2 级),HTML 事件处理程序可以通过访问event
变量得到event
对象。- event 中的属性和方法都是只读的
- 常用属性:
target
事件的目标currentTarget
绑定事件的元素,与 'this' 的指向相同stopPropagation()
取消事件的进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法stopImmediatePropagation()
取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(DOM3级事件中新增)preventDefault()
取消事件的默认行为,比如点击链接跳转。如果cancelable
是true
,则可以使用这个方法type
被触发的事件类型eventPhase
调用事件处理程序的阶段:1表示捕获阶段,2表示“处于目标”,3表示冒泡阶段
this
target
currentTarget
举例:
document.body.onclick = function (event) {
alert(event.currentTarget === document.body); //true
alert(this === document.body); //true
alert(event.target === document.getElementById("myBtn")); //true
};
- 通过
event.type
与switch case
组合,可以通过一个函数处理多个事件。- 只有在事件处理程序执行期间,
event
对象才会存在;一旦事件处理程序执行完成,event
对象就会被销毁。
IE 中的事件对象
- DOM0 级的事件处理程序,
event
作为window
的一个属性存在。(从 IE9 开始,event 可以从参数中获得)attachEvent
添加的事件处理程序,event
作为参数传入,也可以通过window
来访问event
对象。- HTML 事件处理程序依然可以通过访问
event
变量得到event
对象。- 属性和方法:
cancelBubble
设置true
orfalse
可以取消事件冒泡returnValue
设置true
orfalse
可以取消事件的默认行为。srcElement
事件的目标(与DOM中的target
相同)
- 注意事项:
attachEvent
中的event.srcElement === this
吗? 答案是否定的,因为前面说到过attachEvent
中this
指向window
, DOM0 级、DOM2 级 事件处理程序this
才指向event.target / window.event.srcElement
跨浏览器的事件对象
var EventUtil = {
getEvent: function(event){
return event ? event : window.event; // window.event DOM0级时IE
},
getTarget: function(event){
return event.target || event.srcElement; // event.srcElement for IE
},
preventDefault: function(event){
if (event.preventDefault){
event.preventDefault();
} else {
event.returnValue = false; // IE
}
},
stopPropagation: function(event){
if (event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true; // IE
}
}
};
事件类型
- DOM3 级事件规定了几类事件;HTML5 也定义了一组事件;还有一些事件没有规范,浏览器的实现不一致。
- DOM3 级事件模块在 DOM2 级事件模块基础上重新定义了这些事件,也添加了一些新事件。包括 IE9 在内的所有主流浏览器都支持 DOM2 级事件。IE9 也支持 DOM3 级事件。
这里只总结一些常见的事件类型
UI事件类型
- load 事件,当页面完全加载后(包括所有图像、JavaScript 文件、CSS 文件等外部资源),就会触发
window
上面的 load 事件。
EventUtil.addHandler(window, "load", function(){
var image = document.createElement("img");
EventUtil.addHandler(image, "load", function(event){
event = EventUtil.getEvent(event);
alert(EventUtil.getTarget(event).src);
});
document.body.appendChild(image);
image.src = "smile.gif"; //在此之前要先指定事件处理程序
});
- script 元素也会触发 load 事件,据此可以判断动态加载的 JavaScript 文件是否加载完毕。与图像不同,只有在设置了 script 元素的 src 属性并将该元素添加到文档后,才会开始下载 JavaScript 文件
- IE8 及更早版本不支持 script 元素上的 load 事件。
- 在不属于 DOM 文档的图像(包括未添加到文档的 img 元素和 Image 对象)上触发 load 事件时,IE8 及之前版本不会生成 event 对象。IE9 修复了这个问题。
- resize 事件
- 浏览器窗口大小发生变化时会触发该事件,这个事件在
window
上触发,IE、Safari、Chrome 和 Opera 会在浏览器窗口变化了 1 像素时就触发 resize 事件,然后随着变化不断重复触发。Firefox 则只会在用户停止调整窗口大小时才会触发。- 注意不要在这个事件的处理程序中加入大计算量的代码,或者采用函数节流的方式优化性能。
- 浏览器窗口最小化或最大化时也会触发 resize 事件。
- scroll 事件
- 该事件在 window 上发生,此处和书上讲的有点不一样,webkit 内核或 blink 内核的浏览器(Chrome、Opera、Safari)可以通过 document.body.scrollTop 获取页面被卷去的高度,而 Trident、Gecko (IE、火狐)可以通过 document.documentElement.scrollTop来获取该值。
- 另外标准模式、混杂模式这两种方法还有出入,此处不讨论。
- 所以最好通过
document.body.scrollTop + document.documentElement.scrollTop
的方式获取 scrollTop 的值,因为两者之一会等于0,或者使用document.body.scrollTop || document.documentElement.scrollTop
,两者效果一致。
焦点事件
- 这里忽略 DOMFocusIn、DOMFocusOut,因为只有 Opera 支持这个事件,且 DOM3 级事件废弃了它们。
- blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
- focus:在元素获得焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
- focusin:与 focus 等价,但它冒泡。
- focusout:与 blur 等价,也冒泡。
- 支持 focusin、focusout 的浏览器有:IE5.5+、Safari 5.1+、Opera 11.5+和Chrome。但只支持 DOM2 级事件处理程序
- Firefox 不支持 focusin、focusout
- blur、focusout 的事件目标是失去焦点的元素;focus、focusin 的事件目标是获得焦点的元素
鼠标与滚轮事件
click
在用户单击住鼠标按钮或按下回车键时触发。 触发顺序 mousedown mouseup click,如果 mousedown、mouseup 其中之一被取消,就不会触发 click 事件。dblclick
触发顺序 mousedown mouseup click mousedown mouseup click dblclick, 如果中间有事件被取消,dblclick 也不会被触发mousedown
用户按下了任意鼠标按钮时触发。mouseup
用户释放按钮时触发mouseenter
在鼠标光标从元素外部首次移动到元素范围之内时触发。不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事,但 DOM3 级事件将它纳入了规范。IE、Firefox9+和Opera支持这个事件。mouseleave
在位于元素上方的鼠标光标移动到元素范围之外时触发。不冒泡,而且在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事,但 DOM3 级事件将它纳入了规范。IE、Firefox9+ 和 Opera 支持这个事件。mouseover
在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。mouseout
在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
- 用代码说明一下 mouseenter、mouseleave 和 mouseover、mouseout 的区别:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>test1</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="test1.css">
</head>
<body>
<div class="mouseover">
<div class="sub-mouseover">
</div>
</div>
<div class="mouseenter">
<div class="sub-mouseenter">
</div>
</div>
<script src="test1.js"></script>
</body>
</html>
.wrap {
width: 200px;
height: 100px;
}
.mouseover {
background: pink;
}
.mouseenter {
margin-top: 30px;
background: gray;
}
.sub-mouseover,
.sub-mouseenter {
width: 100px;
height: 50px;
background: #AE81FF;
}
var div1 = document.querySelector(".mouseover"),
div2 = document.querySelector(".mouseenter");
div1.addEventListener("mouseover", function(){
console.log("div1 mouseover");
});
div1.addEventListener("mouseout", function(){
console.log("div1 mouseout");
});
div2.addEventListener("mouseenter", function(){
console.log("div2 mouseenter");
});
div2.addEventListener("mouseleave", function(){
console.log("div2 mouseleave");
});
-
效果图
-
鼠标由左侧从上到下依次经过所有 div 的情况,输出
div1 mouseover
div1 mouseout
div1 mouseover
div1 mouseout
div2 mouseenter
div2 mouseleave
mousemove
当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。- 除了 mouseenter、mousedleave,所有鼠标事件都会冒泡,取消鼠标事件将会影响浏览器的默认行为,也会影响其它事件,因为鼠标事件与其它事件是密不可分的。
- 关于
dblclick
IE8 及之前版本中的实现有一个小bug,因此在双击事件中,会跳过第二个mousedown 和click事件,其顺序如下:mousedown
mouseup
click
mouseup
dblclick
,但还是会触发dblclick
事件- 客户区坐标位置:鼠标事件中的
event
都有clientX
clientY
属性,表示在视口中客户区的坐标位置,这些值不包括页面滚动的距离,因此这个位置并不表示鼠标在页面上的位置:
- 页面坐标位置:pageX、pageY,这两个属性表示鼠标光标在页面中的位置,在页面没有滚动的情况下,pageX 和 pageY 的值与 clientX、clientY 的值相等。IE8 及更早版本不支持事件对象上的页面坐标,不过使用客户区坐标和滚动信息可以计算出来。这时候需要用到document.body(混杂模式)或document.documentElement(标准模式)中的scrollLeft 和scrollTop 属性。计算过程如下所示:
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var pageX = event.pageX,
pageY = event.pageY;
if (pageX === undefined){
pageX = event.clientX + (document.body.scrollLeft ||
document.documentElement.scrollLeft);
}
if (pageY === undefined){
pageY = event.clientY + (document.body.scrollTop ||
document.documentElement.scrollTop);
}
alert("Page coordinates: " + pageX + "," + pageY);
});
- 屏幕坐标位置:screenX、screenY
- 修改键 用户按住Shift、Ctrl、Alt、Meta(Windows或Cmd,cmd(mac))时触发鼠标事件,可以在
event
中获得修改键。
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "click", function(event){
event = EventUtil.getEvent(event);
var keys = new Array();
if (event.shiftKey){
keys.push("shift");
}
if (event.ctrlKey){
keys.push("ctrl");
}
if (event.altKey){
keys.push("alt");
}
if (event.metaKey){
keys.push("meta");
}
alert("Keys: " + keys.join(","));
});
- IE9、Firefox、Safari、Chrome 和Opera 都支持这4 个键。IE8 及之前版本不支持metaKey 属性。另外,旧版本的 IE 有自己的一套写法。
- 相关元素
mouseover
mouseout
时的 event.relatedTarget,不做详细记录。- 鼠标按钮
mousedown
mouseup
是在按下/释放任意鼠标按钮时触发的,所以通过 event.button: 0(左) 1(中) 2(右) 可以判断按的是哪个键,但是IE8 及更低版本的浏览器不支持,有兼容写法,此处不详细叙述。EventUtil.getButton
有详细实现。- mousewheel
event.whellDelta
为正数时,向前滚动(回到顶部、页面向下滑动),负数则反过来,这个值是120的倍数,Opera低版本中正负相反,火狐中有自己的一套方法,这里不做详细记录。- 触摸设备
- 不支持dblclick 事件。双击浏览器窗口会放大画面,而且没有办法改变该行为。
- 轻击可单击元素会触发mousemove 事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生mousedown、mouseup 和click 事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了onclick 事件处理程序的元素。
- mousemove 事件也会触发mouseover 和mouseout 事件。
- 两个手指放在屏幕上且页面随手指移动而滚动时会触发mousewheel 和scroll 事件。
- 无障碍性问题
- 如果需要考虑这个问题,不建议使用
click
之外的鼠标事件。因为这个不能通过键盘触发,不利于屏幕阅读器访问。此处不详细记录。
键盘与文本事件
keydown
: 当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发此事件。keypress
当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。按下Esc 键也会触发这个事件。Safari 3.1 之前的版本也会在用户按下非字符键时触发keypress事件。keyup
:当用户释放键盘上的键时触发。- 触发顺序:
keydown
、keypress
、keyup
,keydown
、keypress
都是在文本框发生变化之前被触发的;keyup
事件则是在文本框已经发生变化之后被触发的。- 如果用户按下了一个字符键不放,就会重复触发 keydown 和keypress 事件,直到用户松开该键为止。
- 键盘事件也支持修改键(ctrl等)
- keydown、keyup 中的 event 有 keyCode, 与ASCII 码中对应小写字母或数字的编码相同。
- keypress 中的 event 有 charCode,这个值是按下的那个键所代表字符的 ASCII 编码,用
String.fromCharCode()
可以转换成实际的字符- DOM3 级中,有
key
和char
,其中key
可以直接得到 "k"、"K"、"Shift" 等,char
属性在按下字符键时行为与key
相同,在按下非字符键时为null
,但是支持还不完整,chrome 总是输出 undefined。keyIdentifier
Chrome 已经不推荐使用- 表示按下的按键在键盘的位置,比如按下左右侧的shift键,这个值就不同,Chrome 和 Safari 的实现有 bug。
textInput
: 在文本插入文本框之前会触发textInput 事件。目的是代替keypress,退格键不会触发textInput,但是会触发keypress(只要改变文本),只有真正可以编辑的区域才会触发textInput,但是keypress获得焦点即可触发。event.data中包含用户的输入,拼音输入法中输入过程的拼音不会触发该事件。- inputMethod 代表用户是怎样输入的,比如通过粘贴的方式,但是支持的浏览器很少。
变动事件
DOM2 级的变动(mutation)事件能在 DOM 中的某一部分发生变化时给出提示,比如 DOM 节点的插入、移除、特性被修改等等
HTML5 事件
- contextmenu 事件
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";
});
});
- beforeunload 事件,用户关闭标签页时提示
EventUtil.addHandler(window, "beforeunload", function(event){
event = EventUtil.getEvent(event);
var message = "I'm really going to miss you if you go.";
event.returnValue = message;
return message;
});
- DOMContentLoaded 在形成完整DOM树之后就会触发,不理会图像、JavaScript 文件、CSS 文件或其它资源是否已经下载完毕。其实更应该使用 DOMContentLoaded 而不是 window.onload:
EventUtil.addHandler(window, "DOMContentLoaded", function(event){
alert("Content loaded.");
});
EventUtil.addHandler(window, "load", function(event){
alert("Window loaded.");
});
- IE9+、Firefox、Chrome、Safari 3.1+ 和 Opera9+ 都支持 DOMContentLoaded 事件。
- readystatechange 事件,略。
- pageshow 和 pagehide 事件,此处要了解 Firefox 和 Opera 有一个特性叫 “往返缓存”(back-forward cache/bfcache),用户点击“前进”、“后退”按钮时,会将页面缓存在内存。不重新加载,JavaScript的状态会保留。但是无论页面是否来自 bfcache,都会触发 pageshow 事件,pageshow 的事件处理程序的 event 对象中有
event.persisted
属性,为true
代表页面来自bfcache,同样 pagehide 事件触发时,如果页面被保存到 bfcache 中,则该属性为 true。支持pageshow、pagehide 事件的浏览器有 Firefox、Safari5+、Chrome 和 Opera。 IE9 及以前的版本不支持这两个事件。指定了 onunload 事件处理程序的页面会被自动排除在 bfcache 之外。- hashchange 事件。在 window 上触发,event 包含 oldURL、newURL 两个属性。支持该事件的有 IE8+、Firefox3.6+、Safari5+、Chrome 和 Opera10.6+,但oldURL、newURL只有Firefox6+、Chrome和Opera支持。所以最好用 location 来指定当前的 hash:
EventUtil.addHandler(window, "hashchange", function(event){
console.log(location.hash);
});
设备事件
- orientationchange 事件,屏幕转动。
触摸与手势事件
- touchstart: 当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
- touchmove: 当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault() 可以阻止滚动。
- touchend:当手指从屏幕上移开时触发。
- touchcancel:当系统停止跟踪触摸时触发。关于此事件的确切触发时间,文档中没有明确说明。
- event 对象中包含的常见 DOM 属性有:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和metaKey。
- event 对象中还包含以下用于跟踪触摸的属性:
- touches:表示当前跟踪的触摸操作的Touch 对象的数组。
- targetTouchs:特定于事件目标的Touch 对象的数组。
- changeTouches:表示自上次触摸以来发生了什么改变的Touch 对象的数组。每个Touch 对象包含下列属性:clientX、clientY、pageX、pageY、screenX、screenY、target、identifier(标识触摸的唯一ID)
function handleTouchEvent(event) {
//only for one touch
if (event.touches.length == 1) {
var output = document.getElementById("output");
switch (event.type) {
case "touchstart":
output.innerHTML = "Touch started (" + event.touches[0].clientX + "," + event.touches[0].clientY + ")";
break;
case "touchend":
output.innerHTML += "<br>Touch ended (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
break;
case "touchmove":
event.preventDefault(); //prevent scrolling
output.innerHTML += "<br>Touch moved (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
break;
}
}
}
- 一次触摸的事件触发顺序为:touchstart、mouseover、mousemove(一次)、mousedown、mouseup、click、touchend
- 手势事件:
- gesturestart:当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发。
- gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。
- gestureend:当任何一个手指从屏幕上面移开时触发。
- 属性有标准的鼠标事件属性,还有两个:rotation(正值表示顺时针)和scale(从1开始)
内存和性能
- 每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。
- 必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
事件委托
<body>
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
<script type="text/javascript">
(function () {
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;
}
});
})();
</script>
</body>
- 上面的方法只取得了一个 DOM 元素,只添加了一个事件处理程序,占用的内存更少。
- 如果将事件委托到 document 中,会更有优势:
- document 对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待 DOMContentLoaded 或 load 事件)。
- 在页面中设置事件处理程序所需的时间少。只添加一个事件处理程序所需的 DOM 引用更少,所花的时间也更少。
- 整个页面占用的内存空间更少,能够提升整体性能。
- 最适合采用事件委托技术的事件包块
click
、mousedown
、mouseup
、keydown
、keyup
和keypress
移除事件处理程序
- 如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,因为有的浏览器(尤其是 IE)不会作出恰当地处理,它们很有可能会将对元素和对事件处理程序的引用都保存在内存中。
- IE8 及更早的版本在页面被卸载(刷新,切换页面)之前没有清理干净事件处理程序,它们会滞留在内存中,可以通过 onunload 事件处理程序移除所有事件处理程序。
模拟事件
- 在测试 Web 应用程序,模拟触发事件是一种极其有用的技术。DOM2 级规范为此规定了模拟特定事件的方式,IE9、Opera、Firefox、Chrome 和 Safari 都支持这种方式。IE有它自己模拟事件的方式(IE8 及以下才要用到)
DOM 中的事件模拟
- 可以在 document 对象上使用 createEvent 方法创建 event 对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在 DOM2 级中,所有这些字符串都使用英文复数形式,而在 DOM3 级中变成了单数。这个字符串可以是下列几个字符串之一:
- UIEvents,DOM3 级中是 UIEvent
- MouseEvents: 一般化的鼠标事件,DOM3 级中是 MouseEvent
- MutationEvents: 一般化的 DOM 变动事件。 ...
- HTMLEvents 一般化的 HTML 事件。没有对应的 DOM3 级事件(HTML 事件被分割到其他类别中)
模拟鼠标事件
- createEvent 方法返回的 event 对象中,有 initMouseEvent() 方法,需要传 15 个参数。type(比如"click"),bubbles(Boolean) 是否冒泡,应该设置为 true, cancelable(Boolean) 应该设置为 true,view(几乎总是document.defaultView), detail(通常设置为0), screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button(表示按下了哪个鼠标,默认0), relatedTarget(只有在模拟 mouseover 或 mouseout时使用)
- 将 event 对象传给 DOM 节点的 dispatchEvent 方法即可触发事件,如下:
<body>
<input type="button" value="Click me" id="myBtn"/>
<input type="button" value="Send click to the other button" id="myBtn2"/>
<p>This example works in DOM-compliant browsers (not IE).</p>
<script type="text/javascript">
(function () {
var btn = document.getElementById("myBtn");
var btn2 = document.getElementById("myBtn2");
EventUtil.addHandler(btn, "click", function (event) {
alert("Clicked!");
alert(event.screenX); //100
});
EventUtil.addHandler(btn2, "click", function (event) {
//create event object
var event = document.createEvent("MouseEvents");
//initialize the event object
event.initMouseEvent("click", true, true, document.defaultView, 0, 100, 0, 0, 0, false,
false, false, false, 0, btn2);
//fire the event
btn.dispatchEvent(event);
});
})();
</script>
</body>
模拟键盘事件
- "DOM2 级事件"的草案中本来包含了键盘事件,但在定稿前又被删除了;Firefox 根据其草案实现了键盘事件。但跟 "DOM3 级事件"中的键盘事件有很大区别。
- DOM3 级规定,调用
createEvent()
并传入 "KeyboardEvent" ,返回键盘事件,有initKeyEvent()
方法。这个方法接收一下参数- type, bubbles, cancelable, view, key(按下的键的键码), location(按下了哪里的键,0:主键盘,1:左,2:右,3:数字键盘,4:虚拟键盘,5:手柄), modifiers: 空格分隔的修改键列表,如 "Shift", repeat(在一行中按了这个键多少次)
DOM3 级不提倡keypress
事件, 因此只能模拟keydown
keyup
IE 中的事件模拟
第一步:document.createEventObject()
第二步: 通过赋值的方式初始化事件对象,就是 event.screenX = 0
这些
第三步:btn.fireEvent("onclick", event);
关于标准
-
由于标准在变,现在 DOM3 级事件已经不推荐使用
document.createEvent
的方式,也不推荐通过 event 对象initKeyEvent
或者initKeybordEvent
,书中的跨浏览器代码在狐火中报错了,因为火狐开始支持 DOM3 级事件,标准又在变,现在 DOM3 级标准推荐通过构造函数的方式初始化模拟事件,但这也还是草案。 -
关于跨浏览器模拟事件,粗略了解一下 jQuery 的做法,使用了很多 hack,让本来不冒泡的 focus、blur 可以做事件委托,里面的内容还是很多,得另外总结一下。
-
期待标准被普及的一天: