JavaScript 的事件是以一种流的形式存在的,一个事件会有多个元素同时响应。具体分为:捕获型事件、冒泡型事件(所有浏览器都支持)。
捕获型事件是自上而下的,而冒泡型事件是自下而上的。下面这张图说的很明了:
1.捕获型事件:
<html>
<body>
<div id="div1" style="width:300px;height:300px;background-color:red;">
#div1
<div id="div2" style="width:200px;height:200px;background-color:yellow;margin-top:30px;">
#div2
</div>
</div>
</body>
<script>
var a = document.getElementById('div1');
var b = document.getElementById('div2');
a.addEventListener('click', alertID, true);
b.addEventListener('click', alertID, true);
function alertID() {
alert(this.id);
}
</script>
</html>
2.冒泡型事件:
一个典型的例子:
<html>
<head>
<script type="text/javascript">
var i = 1;
function Add(sText,objText)
{
document.getElementById("Console").innerHTML +=sText + "执行顺序:" + i + "<br />" + "<hr />";
i = i + 1;
//window.event.cancelBubble = true;
}
</script>
</head>
<body οnclick="Add('body事件触发<br />','body')">
<div οnclick="Add('div事件触发<br />','div')">
<p οnclick="Add('p事件触发<br />','p')" style="background:#c00;">点击</p>
</div>
<div id="Console" style="border:solid 1px #ee0; background:#ffc;"></div>
</body>
</html>
DOM0事件模型
事件模型在不断发展,早期的事件模型称为DOM0级别。
DOM0事件模型,所有的浏览器都支持。
直接在dom对象上注册事件名称,就是DOM0写法,比如:
1 document.getElementById("test").onclick = function(e){};
意思就是注册一个onclick事件。当然,它和这种写法是一个意思:
1 document.getElementById("test")["onmousemove"] = function(e){};
这没什么,只不过是两种访问js对象属性的方法,[]的形式主要是为了解决属性名不是合法的标识符,比如:object.123肯定报错,但是object["123"]就避免了这个问题,与此同时,[]的写法,也把js写活了,用字符串表示属性名称,可以在运行时动态绑定事件。
言归正传,事件被触发时,会默认传入一个参数e,表示事件对象,通过e,我们可以获取很多有用的信息,比如点击的坐标、具体触发该事件的dom元素等等。
基于DOM0的事件,对于同一个dom节点而言,只能注册一个,后边注册的同种事件会覆盖之前注册的。例如:
1 var btn = document.getElementById("test"); 2 3 btn.onmousemove = function(e){ 4 alert("ok"); 5 }; 6 7 btn["onmousemove"] = function(e){ 8 alert("ok1"); 9 };
结果会输出ok1。
接下来再说说this。事件触发时,this就是指该事件在哪个dom对象上触发。例如:
1 var btn = document.getElementById("test"); 2 3 btn.onmousemove = function(e){ 4 alert(this.id); 5 };
结果输出test。因为事件就是在id为test的dom节点上注册的,事件触发时,this当然代表这个dom节点,可以理解为事件是被这个dom节点调用的。
所以,想解除事件就相当简单了,只需要再注册一次事件,把值设成null,例如:
1 var btn = document.getElementById("test"); 2 3 btn.onclick = function(e){ 4 alert("ok"); 5 }; 6 7 btn.onclick = null;
原理就是最后注册的事件要覆盖之前的,最后一次注册事件设置成null,也就解除了事件绑定。
事情还没结束,DOM0事件模型还涉及到直接写在html中的事件。例如:
1 <div id="test" class="test" onclick="exec();" ></div>
通过这种方式注册的事件,同样遵循覆盖原则,同样只能注册一个,最后一个生效。
区别就是,这样注册的事件,相当于动态调用函数(有点eval的意思),因此不会传入event对象,同时,this指向的是window,不再是触发事件的dom对象。
DOM2事件模型
DOM2事件模型相对于DOM0,小菜仅仅了解如下两点:
· DOM2支持同一dom元素注册多个同种事件。
· DOM2新增了捕获和冒泡的概念。
DOM2事件通过addEventListener和removeEventListener管理,当然,这是标准。
但IE8及其以下版本浏览器,自娱自乐,搞出了对应的attachEvent和detachEvent,由于小菜才疏学浅,本文不做讨论。
addEventListener当然就是注册事件,她有三个参数,分别为:"事件名称", "事件回调", "捕获/冒泡"。举个例子:
1 var btn = document.getElementById("test"); 2 3 btn.addEventListener("click", function(e){ 4 alert("ok"); 5 }, false);
事件名称就不用多说了,相比DOM0,去掉了前边的on而已。
事件回调也很好理解,事件触发了总得通知你吧!回调时和DOM0一样,也会默认传入一个event参数,同时this是指触发该事件的dom节点。
最后一个参数是布尔型,true代表捕获事件,false代表冒泡事件。其实很好理解,先来个示意图:
意思就是说,某个元素触发了某个事件,最先得到通知的是window,然后是document,依次而入,直到真正触发事件的那个元素(目标元素)为止,这个过程就是捕获。接下来,事件会从目标元素开始起泡,再依次而出,直到window对象为止,这个过程就是冒泡。
为什么要这样设计呢?这貌似是由于深厚的历史渊源,小菜也不怎么了解,就不乱说了。
由此可以看出,捕获事件要比冒泡事件先触发。
假设有这样的html结构:
1 <div id="test" class="test"> 2 <div id="testInner" class="test-inner"></div> 3 </div>
然后我们在外层div上注册两个click事件,分别是捕获事件和冒泡事件,代码如下:
1 var btn = document.getElementById("test"); 2 3 //捕获事件 4 btn.addEventListener("click", function(e){ 5 alert("ok1"); 6 }, true); 7 8 //冒泡事件 9 btn.addEventListener("click", function(e){ 10 alert("ok"); 11 }, false);
最后,点击内层的div,先弹出ok1,后弹出ok。结合上边的原理图,外层div相当于图中的body,内层div相当于图中最下边的div,证明了捕获事件先执行,然后执行冒泡事件。
为什么要强调点击内层的div呢?因为真正触发事件的dom元素,必须是内层的,外层dom元素才有机会模拟捕获事件和冒泡事件,从原理图上就看出了。
如果在真正触发事件的dom元素上注册捕获事件和冒泡事件呢?
html结构同上,js代码如下:
1 var btnInner = document.getElementById("testInner"); 2 3 //冒泡事件 4 btnInner.addEventListener("click", function(e){ 5 alert("ok"); 6 }, false); 7 8 //捕获事件 9 btnInner.addEventListener("click", function(e){ 10 alert("ok1"); 11 }, true);
当然还是点击内层div,结果是先弹出ok,再弹出ok1。理论上应该先触发捕获事件,也就是先弹出ok1,但是这里比较特殊,因为我们是在真正触发事件的dom元素上注册的事件,相当于在图中的div上注册,由图可以看出真正触发事件的dom元素,是捕获事件的终点,是冒泡事件的起点,所以这里就不区分事件了,哪个先注册,就先执行哪个。本例中,冒泡事件先注册,所以先执行。
这个道理适用于多个同种事件,比如说一下子注册了3个冒泡事件,那么执行顺序就按照注册的顺序来,先注册先执行。例如:
1 var btnInner = document.getElementById("testInner"); 2 3 btnInner.addEventListener("click", function(e){ 4 alert("ok"); 5 }, false); 6 7 btnInner.addEventListener("click", function(e){ 8 alert("ok1"); 9 }, false); 10 11 btnInner.addEventListener("click", function(e){ 12 alert("ok2"); 13 }, false);
结果当然是依次弹出ok、ok1、ok2。
为了进一步理解事件模型,还有一种场景,假如说外层div和内层div同时注册了捕获事件,那么点击内层div时,外层div的事件一定是先触发的,代码如下:
1 var btn = document.getElementById("test"); 2 var btnInner = document.getElementById("testInner"); 3 4 btnInner.addEventListener("click", function(e){ 5 alert("ok"); 6 }, true); 7 8 btn.addEventListener("click", function(e){ 9 alert("ok1"); 10 }, true);
结果是先弹出ok1。
假如外层div和内层div都是注册的冒泡事件,点击内层div时,一定是内层div事件先执行,原理相同。
细心的读者会发现,对于div嵌套的情况,如果点击内层的div,外层的div也会触发事件,这貌似会有问题!
点击的明明是内层div,但是外层div的事件也触发了,这的确是个问题。
其实,事件触发时,会默认传入一个event对象,前边提过了,这个event对象上有一个方法:stopPropagation,通过此方法,可以阻止冒泡,这样外层div就接收不到事件了。代码如下:
1 var btn = document.getElementById("test"); 2 var btnInner = document.getElementById("testInner"); 3 4 btn.addEventListener("click", function(e){ 5 alert("ok1"); 6 }, false); 7 8 btnInner.addEventListener("click", function(e){ 9 //阻止冒泡 10 e.stopPropagation(); 11 alert("ok"); 12 }, false);
终于要说说怎么解除事件了。解除事件语法:btn.removeEventListener("事件名称", "事件回调", "捕获/冒泡");
这和绑定事件的参数一样,详细说明下:
· 事件名称,就是说解除哪个事件呗。
· 事件回调,是一个函数,这个函数必须和注册事件的函数是同一个。
· 事件类型,布尔值,这个必须和注册事件时的类型一致。
也就是说,名称、回调、类型,三者共同决定解除哪个事件,缺一不可。举个例子:
1 var btn = document.getElementById("test"); 2 //将回调存储在变量中 3 var fn = function(e){ 4 alert("ok"); 5 }; 6 //绑定 7 btn.addEventListener("click", fn, false); 8 9 //解除 10 btn.removeEventListener("click", fn, false);
要想注册过的事件能够被解除,必须将回调函数保存起来,否则无法解除。
DOM0与DOM2混用
事情本来就很乱了,这又来个混合使用,还让不让人活了。。。
别怕,混合使用完全没问题,DOM0模型和DOM2模型各自遵循自己的规则,互不影响。
整体上来说,依然是哪个先注册,哪个先执行,其他就没什么了。
后记
Event 对象
Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态。
事件通常与函数结合使用,函数不会在事件发生前被执行!
事件句柄 (Event Handlers)
HTML 4.0 的新特性之一是能够使 HTML 事件触发浏览器中的行为,比如当用户点击某个 HTML 元素时启动一段 JavaScript。下面是一个属性列表,可将之插入 HTML 标签以定义事件的行为。
属性 | 此事件发生在何时... |
---|---|
onabort | 图像的加载被中断。 |
onblur | 元素失去焦点。 |
onchange | 域的内容被改变。 |
onclick | 当用户点击某个对象时调用的事件句柄。 |
ondblclick | 当用户双击某个对象时调用的事件句柄。 |
onerror | 在加载文档或图像时发生错误。 |
onfocus | 元素获得焦点。 |
onkeydown | 某个键盘按键被按下。 |
onkeypress | 某个键盘按键被按下并松开。 |
onkeyup | 某个键盘按键被松开。 |
onload | 一张页面或一幅图像完成加载。 |
onmousedown | 鼠标按钮被按下。 |
onmousemove | 鼠标被移动。 |
onmouseout | 鼠标从某元素移开。 |
onmouseover | 鼠标移到某元素之上。 |
onmouseup | 鼠标按键被松开。 |
onreset | 重置按钮被点击。 |
onresize | 窗口或框架被重新调整大小。 |
onselect | 文本被选中。 |
onsubmit | 确认按钮被点击。 |
onunload | 用户退出页面。 |
鼠标 / 键盘属性
属性 | 描述 |
---|---|
altKey | 返回当事件被触发时,"ALT" 是否被按下。 |
button | 返回当事件被触发时,哪个鼠标按钮被点击。 |
clientX | 返回当事件被触发时,鼠标指针的水平坐标。 |
clientY | 返回当事件被触发时,鼠标指针的垂直坐标。 |
ctrlKey | 返回当事件被触发时,"CTRL" 键是否被按下。 |
metaKey | 返回当事件被触发时,"meta" 键是否被按下。 |
relatedTarget | 返回与事件的目标节点相关的节点。 |
screenX | 返回当某个事件被触发时,鼠标指针的水平坐标。 |
screenY | 返回当某个事件被触发时,鼠标指针的垂直坐标。 |
shiftKey | 返回当事件被触发时,"SHIFT" 键是否被按下。 |
IE 属性
除了上面的鼠标/事件属性,IE 浏览器还支持下面的属性:
属性 | 描述 |
---|---|
cancelBubble | 如果事件句柄想阻止事件传播到包容对象,必须把该属性设为 true。 |
fromElement | 对于 mouseover 和 mouseout 事件,fromElement 引用移出鼠标的元素。 |
keyCode | 对于 keypress 事件,该属性声明了被敲击的键生成的 Unicode 字符码。对于 keydown 和 keyup 事件,它指定了被敲击的键的虚拟键盘码。虚拟键盘码可能和使用的键盘的布局相关。 |
offsetX,offsetY | 发生事件的地点在事件源元素的坐标系统中的 x 坐标和 y 坐标。 |
returnValue | 如果设置了该属性,它的值比事件句柄的返回值优先级高。把这个属性设置为 fasle,可以取消发生事件的源元素的默认动作。 |
srcElement | 对于生成事件的 Window 对象、Document 对象或 Element 对象的引用。 |
toElement | 对于 mouseover 和 mouseout 事件,该属性引用移入鼠标的元素。 |
x,y | 事件发生的位置的 x 坐标和 y 坐标,它们相对于用CSS动态定位的最内层包容元素。 |
标准 Event 属性
下面列出了 2 级 DOM 事件标准定义的属性。
属性 | 描述 |
---|---|
bubbles | 返回布尔值,指示事件是否是起泡事件类型。 |
cancelable | 返回布尔值,指示事件是否可拥可取消的默认动作。 |
currentTarget | 返回其事件监听器触发该事件的元素。 |
eventPhase | 返回事件传播的当前阶段。 |
target | 返回触发此事件的元素(事件的目标节点)。 |
timeStamp | 返回事件生成的日期和时间。 |
type | 返回当前 Event 对象表示的事件的名称。 |
标准 Event 方法
下面列出了 2 级 DOM 事件标准定义的方法。IE 的事件模型不支持这些方法:
方法 | 描述 |
---|---|
initEvent() | 初始化新创建的 Event 对象的属性。 |
preventDefault() | 通知浏览器不要执行与事件关联的默认动作。 |
stopPropagation() | 不再派发事件。 |