Javascript与HTML之间的交互是通过 事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用 侦听器(或处理程序)来预定事件,以便事件发生时执行相应的代码。
事件流
事件流描述的是从页面中接收事件的顺序。主要有两种,一种是IE提出的事件冒泡流,一种是Neiscape Communicator提出的事件捕获流。
事件冒泡
就是时间开始时由具体的元素接受,然后逐级向上传播到较为不具体的结点。
所有现代浏览器都支持事件冒泡,但是有所区别。IE5.5及更早版本中的事件冒泡会跳过<html>
元素(从<body>
直接到document
)。IE9,FireFox,Chrome和Safari则将事件一直冒泡到window对象。
事件捕获
思想主要是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。
事件捕获的用意在于在事件到达预定目标之前捕获他。
DOM事件流
"DOM2级事件"规定的事件流包括3个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。(所有的主要浏览器都已经实现了DOM2级事件从IE9开始
)首先发生的是是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
事件的执行顺序
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style type="text/css">
#wrapDiv, #innerP, #textSpan{
margin: 5px;padding: 5px;box-sizing: border-box;cursor: default;
}
#wrapDiv{
width: 300px;height: 300px;border: indianred 3px solid;
}
#innerP{
width: 200px;height: 200px;border: hotpink 3px solid;
}
#textSpan{
display: block;width: 100px;height: 100px;border: orange 3px solid;
}
</style>
</head>
<body>
<div id="wrapDiv">wrapDiv
<p id="innerP">innerP
<span id="textSpan">textSpan</span>
</p>
</div>
<script>
var wrapDiv = document.getElementById("wrapDiv");
var innerP = document.getElementById("innerP");
var textSpan = document.getElementById("textSpan");
// 测试直接绑定的事件到底发生在哪个阶段
wrapDiv.onclick = function(){
console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
};
// 捕获阶段绑定事件
window.addEventListener("click", function(e){
console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
document.addEventListener("click", function(e){
console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
document.documentElement.addEventListener("click", function(e){
console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
document.body.addEventListener("click", function(e){
console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
wrapDiv.addEventListener("click", function(e){
console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
innerP.addEventListener("click", function(e){
console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
textSpan.addEventListener("click", function(){
console.log("textSpan 冒泡 在捕获之前绑定的")
}, false);
textSpan.onclick = function(){
console.log("textSpan onclick")
};
textSpan.addEventListener("click", function(e){
console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
}, true);
// 冒泡阶段绑定的事件
window.addEventListener("click", function(e){
console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
document.addEventListener("click", function(e){
console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
document.documentElement.addEventListener("click", function(e){
console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
document.body.addEventListener("click", function(e){
console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
wrapDiv.addEventListener("click", function(e){
console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
innerP.addEventListener("click", function(e){
console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
textSpan.addEventListener("click", function(e){
console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
}, false);
</script>
</body>
</html>
点击textSpan一次后
可得:先捕获,后冒泡,捕获从上到下,冒泡从下到上。
又可得onclick
事件发生在冒泡阶段
再看对textSpan
的四个console.log();
可以看到 有一个 console.log("textSpan 冒泡 在捕获之前绑定的")
(代码第62到64行)是在下面的捕获之前执行的。
这里有另一个结论就是:如果对一个没有子元素的元素同时绑定冒泡和捕获,结果执行事件是 遵循Javascript
的执行顺序。如果有子元素,则是先执行捕获,然后再执行冒泡。
换句话说:在元素上同时绑定捕获事件和冒泡事件,如果通过此元素的子级元素触发,则优先触发捕获事件,若不通过此元素的子级元素触发,则按照Javascript
执行顺序触发。
W3C规范中定义了三个事件阶段,依次是捕获阶段,目标阶段,冒泡阶段。如果某个阶段不支持或事件对象的传播被终止,那么该阶段就会被跳过。
如果
Event.bubbles
属性被设置为false
,那么冒泡阶段就会被跳过。如果Event.stopPropagation()
在事件派发前被调用,那么所有的阶段都会被跳过。
在一个事件完成了所有阶段的传播路径后,它的Event.currentTarget
会被设置为null
并且Event.eventPhase
会被设为0。Event
的所有其他属性都不会改变(包括指向事件目标的Event.target
属性)
事件处理程序
事件就是用户或浏览器自身执行的某种动作。而相应某个事件的函数就叫做事件处理程序(或事件侦听器)。为事件指定处理程序的方式有好几种。
HTML事件处理程序
-
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定。
-
<input type="button" value="Click Me" onclick="alert('Clicked')">
-
-
在HTML中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本。
- 因为 事件处理程序中的代码在执行时,有权访问到全局作用域中的任何代码
这样指定事件处理程序有一些特点也有一些缺点:
-
我们不能在JavaScript代码中使用未经转义的HTML语法字符,如&(和号)、""(双引号)、<(小于号)、>(大于号)等等,这个操作又是通过指定onclick特性并将一些javascript代码作为它的值来定义的,所以其中的特殊字符要转义。
-
这样会创建一个封装着元素属性值的函数,这个函数中有一个局部变量event,也就是事件对象。
-
缺点1:存在时差问题。因为用户可能会在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件,故报错。
-
为此,很多HTML事件处理程序都会被封装到
try-catch
块中,以便错误不会浮出水面。<input type="button" value="click me" onclick="try{show();}catch(ex){}">
-
-
缺点2:这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。
- 不同JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。
-
缺点3:HTML和JavaScript代码紧密耦合。如果要更换事件处理程序,就必须改动两个地方(HTML代码和JavaScript代码)
DOM0级事件处理程序
这种方法出现在第四代Web浏览器中,而且至今仍然为所有现代浏览器所支持。
原因一是简单,二是具有跨浏览器的优势。
var button=document.getElementById("button");
button.onclick=function(){
alert("clicked");
}
即我们先在script中取得元素的引用,然后再将一个函数赋值给onclick
事件处理程序。
之前介绍过,事件处理程序即为函数,而button.onclick
这种形式即函数作为了对象的方法。那么对象的方法即事件处理程序是在元素(对象)的作用域中运行而非在全局作用域中运行的,因为方法是属于对象的。(注意:例4中事件处理程序是在全局作用域中运行的)。如果这个函数中存在this关键字,那么this就会指向这个对象。
我们还可以通过 button.onclick = null;
的方式来删除DOM0级事件处理程序。
这个方式解决了HTML事件处理程序的三个缺点,但是它也有两个缺点
- 我们不能给一个元素同时添加两个事件
- 我们不能控制元素的事件流
DOM2级事件处理程序
DOM2级事件处理程序定义了两个方法:
addEventListener()
—添加事件侦听器removeEventListener()
—删除事件侦听器
这两个方法都接收三个参数:要处理的事件名,作为事件处理程序的函数,表示事件流方式的布尔值(true则在捕获阶段调用事件处理程序,false则在冒泡阶段调用事件处理程序)
var button=document.getElementById("button");
button.addEventListener("click",function(){
alert(this.id);
},false);
通过addEventListener()
添加的事件处理程序只能使用removeEventListener()
来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()
添加的匿名函数将无法移除。
大多数情况下,都是讲事件处理程序添加到冒泡阶段,这样可以最大限度的兼容各种浏览器。
IE事件处理程序
IE事件处理程序中有类似与DOM2级事件处理程序的两个方法:
-
attachEvent()
添加 -
detachEvent()
删除
由于IE8及更早版本只支持事件冒泡,所以通过attachEvent()
添加的事件处理程序都会被添加到冒泡阶段。
他们接收2个相同的参数:事件处理程序名称(‘onclick’),事件处理程序函数。
注意:
- IE事件处理程序中
attachEvent()
的事件处理程序的作用域和DOM0与DOM2不同,她的作用域是在全局作用域中。因此,不同于DOM0和DOM2中this指向元素,IE中的this指向window。 - 同样,我们可以使用
attachEvent()
来给同一个元素添加多个事件处理程序。但是与DOM2不同,事件触发的顺序不是添加的顺序而是添加顺序的相反顺序。 - 同样地,通过
attachEvent()
添加的事件处理程序必须通过detachEvent()
方法移除,同样的,不能使用匿名函数。 - 支持IE事件处理程序的浏览器不只有IE浏览器,还有Opera浏览器。
跨浏览器的事件处理程序
var EventUtil={
addHandler:function(element,type,handler){
if(element.addEventListener){
element.addEventListener(type,handler,false);//注意:这里默认使用了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);//注意:这里默认使用了false(冒泡)
}else if(element.detachEvent){
element.detachEvent("on"+type,handler);
}else{
element["on"+type]=null;
}
}
};
要构建两个函数一个是addHandler();
一个是removeHandler()