在IE 3.0和Netscape 2.0浏览器中开始出现事件。DOM 2规范开始标准化DOM事件,直到2004年发布DOM 3.0时,W3C才完善事件模型。目前,所有主流浏览器都支持DOM 2事件模块。IE8及其早期版本还继续使用IE事件模块。
1、事件基础
1.1、事件模型
在浏览器发展历史中,出现以下4种事件处理模型:
- 基本事件模型:也称为DOM 0事件模型。它是浏览器初期出现的一种比较简单的事件模型,主要通过HTML事件属性,为指定标签绑定事件处理函数。由于这种模型应用比较广泛,获得了所有浏览器的支持,目前依然比较流行。但是这种模型对于HTML文档标签依赖严重,不利于JavaScript独立开发。
- DOM事件模型:由W3C制定,是目前标准的事件处理模型。除了IE怪异模式不支持外,符合标准的浏览器都支持该模型。DOM事件模型包括DOM 2事件模块和DOM 3事件模块,DOM 3事件模块为DOM 2事件模块的升级版,较DOM 2事件模块略有完善,主要是新增加一些事情类型,以适应移动设备的开发需要,但大部分规范和用法保持一致。
- IE事件模型:IE 4.0及其以上版本浏览器支持,与DOM事件模型相似,但用法不同。
- Netscape事件模型:由Netscape 4浏览器实现,在Netscape 6中停止支持。
1.2、事件流
事件流就是多个节点对象对同一个事件进行响应的先后顺序,主要包括以下3种类型:
1.冒泡型
事件从最特定的目标向最不特定的目标(document对象)触发,也就是事件从下向上进行响应,这个传递过程被形象地称为冒泡。
2.捕获型
事件从最不特定的目标(document对象)开始触发,然后到最特定的目标,也就是事件从上向下进行响应。
3.混合型
W3C的DOM事件模型支持捕获型和冒泡型两种事件流,其中捕获型事件流先发生,然后发生冒泡型事件流。两种事件流会触及DOM中的所有层级对象,从document对象开始,最后返回document对象结束。
因此,可以把事件传播的整个过程分为3个阶段:
- 捕获阶段:事件从document对象沿着文档树向下传播到目标节点,如果目标节点的任何一个上级节点注册了相同事件,那么事件在传播的过程中就会首先在最接近顶部的上级节点执行,依次向下传播。
- 目标阶段:注册在目标节点的事件被执行。
- 冒泡阶段:事件从目标节点向上触发,如果上级节点注册了相同的事件,将会逐级响应,依次向上传播。
1.3、绑定事件
在基本事件模型中,JavaScript支持两种绑定方式。
1.静态绑定
把JavaScript脚本作为属性值,直接赋予事件属性。
【示例1】把JavaScript脚本以字符串的形式传递给onclick属性,为<button>
标签绑定click事件。当单击按钮时,就会触发click事件,执行这行JavaScript脚本:
<button onclick="alert('你单击了一次!');">按钮</button>
2.动态绑定
使用DOM对象的事件属性进行赋值。
【示例2】使用document.getElementById()方法获取button元素,然后把一个匿名函数作为值传递给button元素的onclick属性,以实现事件绑定操作:
<button id="btn">按钮</button>
<script>
var button = document.getElementById("btn");
button.onclick = function(){
alert("你单击了一次!");
}
</script>
可以在脚本中直接为页面元素附加事件,不破坏HTML结构,比上一种方式灵活。
1.4、事件处理函数
事件处理函数是一类特殊的函数,与函数直接量结构相同,主要任务是实现事件处理,为异步调用,由事件触发进行响应。
事件处理函数一般没有明确的返回值。不过在特定事件中,用户可以利用事件处理函数的返回值影响程序的执行,如单击超链接时,禁止默认的跳转行为。
【示例1】为form元素的onsubmit事件属性定义字符串脚本,设计当文本框中输入值为空时,定义事件处理函数返回值为false,这样将强制表单禁止提交数据:
<form id="form1" name="form1" method="post" action="http://www.mysite.cn/" onsubmit="if(this.elements[0].value.length==0)
return false;">
姓名:<input id="user" name="user" type="text" />
<input type="submit" name="btn" id="btn" value="提交" />
</form>
在上面代码中,this表示当前form元素,elements[0]表示姓名文本框,如果该文本框的value.length属性值长度为0,表示当前文本框为空,则返回false,禁止提交表单。
事件处理函数不需要参数。在DOM事件模型中,事件处理函数默认包含event参数对象,event对象包含事件信息,在函数内进行传播。
【示例2】为按钮绑定一个单击事件。在这个事件处理函数中,参数e为形参,响应事件之后,浏览器会把event对象传递给形参变量e,再把event对象作为一个实参进行传递,读取event对象包含的事件信息,在事件处理函数中输出当前源对象节点名称。实现效果如下图所示:
<button id="btn">按 钮</button>
<script>
var button = document.getElementById("btn");
button.onclick = function(e){
var e = e || window.event; //获取事件对象
document.write(e.srcElement ? e.srcElement : e.target); //获取当前单击对象的标签名
}
</script>
【示例3】定义当单击按钮时改变当前按钮的背景色为红色,其中this关键字表示button按钮对象:
<button id="btn" onclick="this.style.background='red';">按钮
</button>
也可以使用下面一行代码表示:
<button id="btn" onclick="(event.srcElement?event.srcElement:
event.target).style.background='red';">按钮</button>
1.5、注册事件
在DOM事件模型中,通过调用对象的addEventListener()方法注册事件,用法如下:
element.addEventListener(String type, Function listener, boolean useCapture);
参数说明如下:
- type:注册事件的类型名。事件类型与事件属性不同,事件类型名没有on前缀。例如,对于事件属性onclick来说,所对应的事件类型为click。
- listener:监听函数,即事件处理函数。在指定类型的事件发生时将调用该函数。在调用这个函数时,默认传递给它的唯一参数是event对象。
- useCapture:它是一个布尔值。如果为true,则指定的事件处理函数将在事件传播的捕获阶段触发;如果为false,则事件处理函数将在冒泡阶段触发。
提示:使用addEventListener()方法能够为多个对象注册相同的事件处理函数,也可以为同一个对象注册多个事件处理函数。为同一个对象注册多个事件处理函数对于模块化开发非常有用。
【示例1】为段落文本注册两个事件:mouseover和mouseout。当鼠标移到段落文本上面时会显示为蓝色背景,而当鼠标移出段落文本时会自动显示为红色背景。这样就不需要破坏文档结构为段落文本增加多个事件属性:
<p id="p1">为对象注册多个事件</p>
<script>
var p1 = document.getElementById("p1"); //捕获段落元素的句柄
p1.addEventListener("mouseover", function(){
this.style.background = 'blue';
} , true); //为段落元素注册第1个事件处理函数
p1.addEventListener("mouseout", function(){
this.style.background = 'red';
}, true); //为段落元素注册第2个事件处理函数
</script>
IE事件模型使用attachEvent()方法注册事件,用法如下:
element.attachEvent(etype,eventName)
参数说明如下:
- etype:设置事件类型,如onclick、onkeyup、onmousemove等。
- eventName:设置事件名称,也就是事件处理函数。
【示例2】为段落标签
标签注册两个事件:mouseover和mouseout,设计当鼠标经过时,段落文本背景色显示为蓝色,当鼠标移开之后,背景色显示为红色:
<p id="p1">IE事件注册</p>
<script>
var p1 = document.getElementById("p1"); //捕获段落元素
p1.attachEvent("onmouseover", function(){
p1.style.background = 'blue';
}); //注册mouseover事件
p1.attachEvent("onmouseout", function(){
p1.style.background = 'red';
}); //注册mouseout事件
</script>
提示:使用attachEvent()注册事件时,其事件处理函数的调用对象不再是当前事件对象本身,而是window对象,因此事件函数中的this就指向window,而不是当前对象,如果要获取当前对象,应该使用event的srcElement属性。
注意,IE事件模型中的attachEvent()方法第一个参数为事件类型名称,但需要加上on前缀,而使用addEventListener()方法时,不需要这个on前缀,如click。
1.6、销毁事件
在DOM事件模型中,使用removeEventListener()方法可以从指定对象中删除已经注册的事件处理函数。用法如下:
element.removeEventListener(String type, Function listener, boolean useCapture);
参数说明参阅addEventListener()方法参数说明。
【示例1】在下面示例中,分别为按钮a和按钮b注册click事件,其中按钮a的事件函数为ok(),按钮b的事件函数为delete_event()。在浏览器中预览,单击“点我”按钮,将弹出一个对话框,在删除之前这个事件是一直存在的。在单击“删除事件”按钮之后,“点我”按钮将失去任何效果:
<input id="a" type="button" value="点我" />
<input id="b" type="button" value="删除事件" />
<script>
var a = document.getElementById("a"); //获取按钮a
var b = document.getElementById("b"); //获取按钮b
function ok(){ //按钮a的事件处理函数
alert("您好,欢迎光临!");
}
function delete_event(){ //按钮b的事件处理函数
a.removeEventListener("click",ok,false); //移出按钮a的click事件
}
a.addEventListener("click",ok,false); //默认为按钮a注册事件
b.addEventListener("click",delete_event,false); //默认为按钮b注册事件
</script>
提示:removeEventListener()方法只能够删除addEvent-Listener()方法注册的事件。如果直接使用onclick等直接写在元素上的事件,将无法使用removeEventListener()方法删除。
当临时注册一个事件时,可以在处理完毕之后迅速删除它,这样能够节省系统资源。
IE事件模型使用detachEvent()方法注销事件,用法如下:
element.detachEvent(etype,eventName)
参数说明参阅attachEvent()方法参数说明。
由于IE怪异模式不支持DOM事件模型,为了保证页面的兼容性,开发时需要兼容两种事件模型以实现在不同浏览器中具有相同的交互行为。
1.7、使用event对象
event对象由事件自动创建,记录了当前事件的状态,如事件发生的源节点、键盘按键的响应状态、鼠标指针的移动位置、鼠标按键的响应状态等信息。event对象的属性提供了有关事件的细节,其方法可以控制事件的传播。
2级DOM Events规范定义了一个标准的事件模型,它被除了IE怪异模式以外的所有现代浏览器所实现,而IE定义了专用的、不兼容的模型。简单比较两种事件模型:
- 在DOM事件模型中,event对象被传递给事件处理函数,但是在IE事件模型中,它被存储在window对象的event属性中。
- 在DOM事件模型中,event类型的各种子接口定义了额外的属性,它们提供了与特定事件类型相关的细节;在IE事件模型中,只有一种类型的event对象,它用于所有类型的事件。
下面列出了2级DOM事件标准定义的event对象属性,如下表所示。注意,这些属性都是只读属性。
下面列出了2级DOM事件标准定义的event对象方法,如下表所示,IE事件模型不支持这些方法:
1.8、委托事件
事件委托(delegate)也称为事件托管或事件代理,就是把目标节点的事件绑定到祖先节点上。这种简单而优雅的事件注册方式是基于事件传播过程中,逐层冒泡总能被祖先节点捕获。
委托的好处:优化代码,提升运行性能,真正把HTML和JavaScript分离,也能防止在动态添加或删除节点过程中,注册事件丢失的现象。
【示例1】使用一般方法为列表结构中每个列表项目绑定click事件,单击列表项目,将弹出提示对话框,提示当前节点包含的文本信息。但是,当我们为列表框动态添加列表项目之后,新添加的列表项目没有绑定click事件,这与我们的愿望相反:
<button id="btn">添加列表项目</button>
<ul id="list">
<li>列表项目1</li>
<li>列表项目2</li>
<li>列表项目3</li>
</ul>
<script>
var ul=document.getElementById("list");
var lis=ul.getElementsByTagName("li");
for(var i=0;i<lis.length;i++){
lis[i].addEventListener('click',function(e){
var e = e || window.event;
var target = e.target || e.srcElement;
alert(e.target.innerHTML);
},false);
}
var i = 4;
var btn=document.getElementById("btn");
btn.addEventListener("click",function(){
var li = document.createElement("li");
li.innerHTML = "列表项目" + i++;
ul.appendChild(li);
});
</script>
【示例2】下面示例借助事件委托技巧,利用事件传播机制,在列表框ul元素上绑定click事件,当事件传播到父节点ul上时,捕获click事件,然后在事件处理函数中检测当前事件响应节点类型,如果是li元素,则进一步执行下面代码,否则跳出事件处理函数,结束响应:
<button id="btn">添加列表项目</button>
<ul id="list">
<li>列表项目1</li>
<li>列表项目2</li>
<li>列表项目3</li>
</ul>
<script>
var ul=document.getElementById("list");
ul.addEventListener('click',function(e){
var e = e || window.event;
var target = e.target || e.srcElement;
if(e.target&&e.target.nodeName.toUpperCase()=="LI"){ /*判断目标事件是否为li*/
alert(e.target.innerHTML);
}
},false);
var i = 4;
var btn=document.getElementById("btn");
btn.addEventListener("click",function(){
var li = document.createElement("li");
li.innerHTML = "列表项目" + i++;
ul.appendChild(li);
});
</script>
当页面存在大量元素,并且每个元素注册了一个或多个事件时,可能会影响性能。访问和修改更多的DOM节点,程序就会更慢,特别是事件连接过程都发生在load(或DOMContentReady)事件中时,对任何一个富交互网页来说,这都是一个繁忙的时间段。另外,浏览器需要保存每个事件句柄的记录,也会占用更多内存。
2、实战
2.1、鼠标拖曳
鼠标事件是Web开发中最常用的事件类型,鼠标事件类型详细说明如下表所示:
【示例】下面示例演示了如何综合应用各种鼠标事件实现页面元素拖放操作的设计过程。实现拖放操作设计,需要解决以下几个问题:
- 定义拖放元素为绝对定位,以及设计事件的响应过程。这个比较容易实现。
- 清楚几个坐标概念:单击鼠标时的指针坐标、移动中当前鼠标的指针坐标、松开鼠标时的指针坐标、拖放元素的原始坐标、拖动中的元素坐标。
- 算法设计:单击鼠标时,获取被拖放元素和鼠标指针的位置,在移动中实时计算鼠标偏移的距离,并利用该偏移距离加上被拖放元素的原坐标位置,获得拖放元素的实时坐标。
如下图所示,其中变量ox和oy分别记录按下鼠标时被拖放元素的横、纵坐标值,它们可以通过事件对象的offsetLeft和offsetTop属性获取。变量mx和my分别表示按下鼠标时,鼠标指针的坐标位置。而event.mx和event.my是事件对象的自定义属性,用它们来存储当鼠标移动时鼠标指针的实时位置。
当获取了上面3对坐标值之后,就可以动态计算拖动中元素的实时坐标位置,即x轴值为ox +event.mx – mx,y轴值为oy + event.my – my。当释放鼠标按钮时,则可以释放事件类型,并记下松开鼠标指针时拖动元素的坐标值,以及鼠标指针的位置,留待下一次拖放操作时调用。
完整拖放操作的示例代码如下:
<div id="box" ></div>
<script>
//初始化拖放对象
var box = document.getElementById("box"); //获取页面中被拖放元素的引用指针
box.style.position = "absolute"; //绝对定位
box.style.width = "160px"; //定义宽度
box.style.height = "120px"; //定义高度
box.style.backgroundColor = "red"; //定义背景色
//初始化变量,标准化事件对象
var mx, my, ox, oy; //定义备用变量
function e(event){ //定义事件对象标准化函数
if( ! event){ //兼容IE事件模型
event = window.event;
event.target = event.srcElement;
event.layerX = event.offsetX;
event.layerY = event.offsetY;
}
event.mx = event.pageX || event.clientX + document.body.scrollLeft;
//计算鼠标指针的x轴距离
event.my = event.pageY || event.clientY + document.body.scrollTop;
//计算鼠标指针的y轴距离
return event; //返回标准化的事件对象
}
//定义鼠标事件处理函数
document.onmousedown = function(event){ //按下鼠标时,初始化处理
event = e(event); //获取标准事件对象
o = event.target; //获取当前拖放的元素
ox = parseInt(o.offsetLeft); //拖放元素的x轴坐标
oy = parseInt(o.offsetTop); //拖放元素的y轴坐标
mx = event.mx; //按下鼠标指针的x轴坐标
my = event.my; //按下鼠标指针的y轴坐标
document.onmousemove = move; //注册鼠标移动事件处理函数
document.onmouseup = stop; //注册松开鼠标事件处理函数
}
function move(event){ //鼠标移动处理函数
event = e(event);
o.style.left = ox + event.mx - mx + "px"; //定义拖动元素的x轴距离
o.style.top = oy + event.my - my + "px"; //定义拖动元素的y轴距离
}
function stop(event){ //松开鼠标处理函数
event = e(event);
ox = parseInt(o.offsetLeft); //记录拖放元素的x轴坐标
oy = parseInt(o.offsetTop); //记录拖放元素的y轴坐标
mx = event.mx ; //记录鼠标指针的x轴坐标
my = event.my ; //记录鼠标指针的y轴坐标
o = document.onmousemove = document.onmouseup = null; //释放所有操作对象
}
</script>
2.2、鼠标移动
在下面实例中分别为3个嵌套的div元素定义了mouseover和mouseout事件处理函数,这样从外层的父元素中移动到内部的子元素中时,将会触发父元素的mouseover事件类型,但是不会触发mouseout事件类型。
<div>
<div>
<div>盒子</div>
</div>
</div>
<script>
var div = document.getElementsByTagName("div"); //获取3个嵌套的div元素
for(var i=0;i<div.length;i++){ //遍历嵌套的div元素
div[i].onmouseover = function(e){ //注册移过事件处理函数
this.style.border = "solid blue";
}
div[i].onmouseout = function(){ //注册移出事件处理函数
this.style.border = "solid red";
}
}
</script>
2.3、鼠标定位
当事件发生时,获取鼠标的位置是很重要的事件。由于浏览器的不兼容性,不同浏览器分别在各自事件对象中定义了不同的属性,说明如下表所示。这些属性都以像素值定义了鼠标指针的坐标,但是它们参照的坐标系不同,导致准确计算鼠标的位置比较麻烦。
【示例】下面介绍如何配合使用多种鼠标坐标属性,以实现兼容不同浏览器的鼠标定位设计方案。
首先,来看看screenX和screenY属性。这两个属性获得了所有浏览器的支持,应该说是最优选用属性,但是它们的坐标系是计算机屏幕,也就是说,以计算机屏幕左上角为定位原点。这对于以浏览器窗口为活动空间的网页来说,没有任何价值。因为不同的屏幕分辨率、不同的浏览器窗口大小和位置都使在网页中定位鼠标成为一件很困难的事情。
其次,如果以document对象为坐标系,则可以考虑选用pageX和pageY属性,实现在浏览器窗口中进行定位。这对于设计鼠标跟随是一个好主意,因为跟随元素一般都以绝对定位的方式在浏览器窗口中移动,在mousemove事件处理函数中把pageX和pageY属性值传递给绝对定位元素的top和left样式属性即可。
IE事件模型不支持pageX和pageY属性,为此还需寻求兼容IE的方法。再看看clientX和clientY属性是以window对象为坐标系,且IE事件模型支持它们,可以选用它们。不过考虑Window等对象可能出现的滚动条偏移量,所以还应加上相对于window对象的页面滚动的偏移量。
var posX = 0, posY = 0; //定义坐标变量初始值
var event = event || window.event; //标准化事件对象
if(event.pageX || event.pageY){ //如果浏览器支持该属性,则采用它们
posX = event.pageX;
posY = event.pageY;
}
else if(event.clientX || event.clientY){ //否则,如果浏览器支持该属性,则采用它们
posX = event.clientX + document.documentElement.scrollLeft +
document.body.scrollLeft;
posY = event.clientY + document.documentElement.scrollTop +
document.body.scrollTop;
}
在上面代码中,先检测pageX和pageY属性是否存在,如果存在则获取它们的值;如果不存在,则检测并获取clientX和clientY属性值,然后加上document.documentElement和document.body.对象的scrollLeft和scrollTop属性值,这样在不同浏览器中就获得了相同的坐标值。
2.4、键盘监控
当用户操作键盘时会触发键盘事件,键盘事件主要包括下面3种类型。
- keydown:在键盘上按下某个键时触发。如果按住某个键,会不断触发该事件,但是Opera浏览器不支持这种连续操作。该事件处理函数返回false时,会取消默认的动作(如输入的键盘字符,在IE和Safari浏览器下还会禁止keypress事件响应)。
- keypress:按下某个键盘键并释放时触发。如果按住某个键,会不断触发该事件。该事件处理函数返回false时,会取消默认的动作(如输入的键盘字符)。
- keyup:释放某个键盘键时触发。该事件仅在松开键盘时触发一次,不是一个持续的响应状态。
键盘事件定义了很多属性,如下表所示。利用这些属性可以精确控制键盘操作。键盘事件属性一般只在键盘相关事件发生时才会存在于事件对象中,但是ctrlKey和shiftKey属性除外,因为它们可以在鼠标事件中存在。例如,当按下ctrl或shift键时单击鼠标操作:
【示例】ctrlKey和shiftKey属性可存在于键盘和鼠标事件中,表示键盘上的Ctrl和Shift键是否被按住。下面示例能够监测Ctrl和Shift键是否被同时按下。如果同时按下,且鼠标单击某个页面元素,则会把该元素从页面中删除:
document.onclick = function(e){
var e = e || window.event; //标准化事件对象
var t = e.target || e.srcElement; //获取发生事件的元素,兼容IE和DOM
if(e.ctrlKey && e.shiftKey) //如果同时按下Ctrl和Shift键
t.parentNode.removeChild(t); //移出当前元素
}
2.5、键盘移动对象
keyCode和charCode属性使用比较复杂,但是它们在实际开发中又比较常用,故比较这两个属性在不同事件类型和不同浏览器中的表现是非常必要的,如下表所示。读者可以根据需要有针对性地选用事件响应类型和引用属性值。
某些键的可用性不是很确定,如PageUp和Home键等。不过常用的功能键和字符键都是比较稳定的,如下表所示:
【示例】下面示例演示了如何使用方向键控制页面元素的移动效果:
<div id="box"></div>
<script>
var box = document.getElementById("box"); //获取页面元素的引用指针
box.style.position = "absolute"; //色块绝对定位
box.style.width = "20px"; //色块宽度
box.style.height = "20px"; //色块高度
box.style.backgroundColor = "red"; //色块背景
document.onkeydown = keyDown; //在document对象中注册keyDown事件处理函数
function keyDown(event){ //方向键控制元素移动函数
var event = event || window.event; //标准化事件对象
switch(event.keyCode){ //获取当前按下键盘键的编码
case 37 : //按下左箭头键,向左移动5个像素
box.style.left = box.offsetLeft - 5 + "px";
break;
case 39 : //按下右箭头键,向右移动5个像素
box.style.left = box.offsetLeft + 5 + "px";
break;
case 38 : //按下上箭头键,向上移动5个像素
box.style.top = box.offsetTop - 5 + "px";
break;
case 40 : //按下下箭头键,向下移动5个像素
box.style.top = box.offsetTop + 5 + "px";
break;
}
return false
}
</script>
在上面示例中,首先获取页面元素,然后通过CSS脚本控制元素绝对定位、大小和背景色。然后在document对象上注册鼠标按下事件类型处理函数,在事件回调函数keyDown()中侦测当前按下的方向键,并决定定位元素在窗口中的位置。其中元素的offsetLeft和offsetTop属性可以存取它在页面中的位置。
2.6、页面监控
页面事件主要包括与页面相关的操作响应,常用事件类型说明如下:
- oad事件在页面完全加载完毕的时候触发。
- unload事件在从当前浏览器窗口内移动文档的位置时触发。
- resize事件在浏览器窗口被重置时触发。
- scroll事件在浏览器窗口内移动文档的位置时触发。
- error事件在JavaScript代码发生错误时触发。
【示例】在下面示例中,控制红色小盒子始终位于窗口内坐标为(100px,100px)的位置。
<div id="box"></div>
<script>
var box = document.getElementById("box");
box.style.position = "absolute";
box.style.backgroundColor = "red";
box.style.width = "200px";
box.style.height = "160px";
window.onload = f; //页面初始化时固定其位置
window.onscroll = f; //当文档位置发生变化时重新固定其位置
function f(){ //元素位置固定函数
box.style.left = 100 + parseInt(document.body.scrollLeft) + "px";
box.style.top = 100 + parseInt(document.body.scrollTop) + "px";
}
</script>
<div style="height:2000px;width:2000px;"></div>
还有一种方法,就是利用settimeout()函数实现每间隔一定时间校正一次元素的位置,不过这种方法的损耗比较大,不建议选用。