客户端JavaScript程序使用异步事件驱动的编程模型。在这种编程风格下,浏览器会在文档、浏览器或者某些元素或与之关联的对象发生某些值得关注的事情时生成事件。事件往往是元素本身所具备的,我们需要做的是为这些事件做出反馈,也就是当一个事件触发时,程序需要做些什么。在JavaScript中事件可以注册一个或多个函数,让这些函数在该类型事件发生时被调用。
任意具有图形用户界面的应用都是这样的,界面就在那里等待用户与之交互(等待事件的发生),然后给出反馈。
事件类型
事件类型是一个字符串,也叫事件名称,表示发生了什么事。例如,"mousermove
"表示用户移动了鼠标,"keydown
"表示用户按下了键盘上的某个键。而"load
"表示文档(或其他资源)已经通过网络加载完成。
事件目标
事件目标也叫事件源,它是一个对象,事件就发生在该对象上或者与该对象有关。比如window
对象发生了加载(load
)事件,或者一个<button>
元素发生了单机(click
)事件。
注册事件处理函数
有两种注册事件处理函数的方式,第一种方式是设置作为事件目标的对象或文档元素的一个属性。第二种是把事件处理函数传给这个对象或元素的addEventListener()
方法。
设置事件处理程序属性:JavaScript
第一种方式是把事件目标的一个属性设置为关联的事件处理函数,按照惯例,事件处理函数的名字都由on
和事件名称组成,比如onclick
、onload
、onchange
等等,注意这些名称必须全部小写。所以一个完整的事件由以下几个部分组成:
- 元素(事件目标) -> oDiv
- 句柄(事件类型) -> on + eventType
- 处理函数(事件反馈) - > onclick = function(){...}
ini
oDiv.onclick = function(){
this.style.backgroundColor = 'orange';
}
使用属性设置事件处理函数有一个缺点,无法绑定多个同类型事件的处理函数,后一个会覆盖前一个。比如以下代码中,当我们点击div元素时,控制台只会打印出2。
xml
<div>
</div>
<script>
var oDiv = document.getElementsByTagName('div')[0];
oDiv.onclick = function(){
console.log(1); // 不会打印
}
oDiv.onclick = function(){
console.log(2);
}
</script>
设置事件处理程序属性:HTML
文档元素的事件处理函数可以直接在HTML文件中作为对应HTML标签的属性来定义,也叫内联事件监听器(不建议使用,因为结构应该和事件要分开。)
ini
<button onclick="console.log('点击成功')">点击我</button>
addEventListener()
任何可以作为事件目标的对象都定义了一个名为addEventListener()
的方法,可以使用它来注册目标为调用对象的事件处理函数。
addEventListener()
方法 也叫事件监听器,它可以接收三个参数,第一个参数是事件类型,是一个字符串(不需要加on
)。第二个参数是事件处理函数,第三个是可选参数,用于设置事件流类型是否为捕获,默认为fasle
。(IE8
及以下不兼容)。
ini
var oBtn = document.getElementsByTagName('button')[0];
oBtn.addEventListener('click', function(){
this.innerHTML = '加载中...';
var self = this;
setTimeout(function(){
self.innerHTML = '加载完毕';
}, 2000)
}, false);
它可以绑定多个同类型事件的处理函数,执行顺序是按照绑定的先后顺序执行。以下代码会打印两次1。
javascript
var oBtn = document.getElementsByTagName('button')[0];
oBtn.addEventListener('click', function(){
console.log(1);
}, false);
oBtn.addEventListener('click', function(){
console.log(1);
}, false);
如果绑定事件的处理函数是同一个,多次绑定也只执行一次,以下代码只会打印一次1。
javascript
var oBtn = document.getElementsByTagName('button')[0];
function test(){
console.log(1);
}
oBtn.addEventListener('click', test, false);
oBtn.addEventListener('click', test, false);
针对IE8版本及以下的浏览器,可以使用attachEvent()
方法注册事件处理函数,它有两个参数,第一个参数是事件类型(需要加on
),第二个参数是事件处理函数。
attachEvent()
一样可以绑定多个同类型事件的处理函数,执行顺序是后绑定先执行(并且如果绑定事件的处理函数是同一个,多次绑定也会多次执行)。但要注意它的处理函数是window
调用的,需要修改this
的指向。
为了兼容性,我们可以对事件监听器做一下封装,注意要修改this
指向。
rust
function addEvent(el, type, fn){
if(el.addEventListener){
el.addEventListener(type, fn, false);
}else if(el.attachEvent){
el.attachEvent('on' + type, function(){
// 使用call修改this指向
fn.call(el);
})
}else{
el['on' + type] = fn;
}
}
调用事件处理程序
注册事件处理函数后,浏览器会在指定对象发生指定事件时自动调用它。
事件处理程序的参数
当事件的处理函数被触发时,浏览器每次都会将一个事件对象(event
)作为参数传递给处理函数。DOM中存在着多种不同类型的事件对象,但是它们有一个共同祖先Event
函数。
注意在IE8
及以下版本的浏览器中,event
是存在window
对象中的,所以要考虑兼容性写法。
javascript
oBtn.addEventListener('click', function(e){
var e = e || window.event;
}, false);
这个event
对象具有很多属性,这些属性提供了事件的详细信息。因为是参数,所以事件对象也可以用其它名字代替,比如常用e
表示。
type
属性 返回发生事件的类型。
target
属性和srcElement
属性 返回触发此事件的目标,谷歌浏览器这两个属性都有,火狐只有target,IE只有srcElement,所以又需要考虑兼容性写法:
ini
oBtn.addEventListener('click', function(e){
var e = e || window.event,
tar = e.target || e.srcElement;
}, false);
currentTarget
属性 返回注册当前事件处理函数的对象(同this
)。
事件对象中封装了当前事件相关的一切信息,比如鼠标事件对象中有鼠标的水平/垂直坐标,键盘事件对象中有按下的键名等。
-
clientX/clientY
属性 返回当前可视窗口的鼠标指针的水平/垂直坐标(IE8
及以下版本不支持)。 -
pageX/pageY
属性 返回相对于整个页面的鼠标指针的水平/垂直坐标,这个方法是包含页面的边框的。(IE8
及以下版本不支持)。
事件处理程序的this指向
在通过设置属性注册事件处理函数时,看起来就像是为事件目标的对象定义了一个新的方法,作为方法调用时,this
自然是指向该对象的,很合理。使用addEventListener()
方法注册也是一样,以事件目标作为其this
值。
不过要注意箭头函数除外,箭头函数中的this
的值始终等定义它的作用域的值。
解绑事件处理程序
JavaScript中事件的解绑一般遵循"用什么方式绑定,就用什么方式解绑",不过需要注意的是,addEventListener()
方法和attachEvent()
方法绑定的函数不能是匿名函数,否则将无法解绑。
elem.onclick = null/fasle
elem.removeEventListener()
elem.detachEvent()
非严格模式下可以用callee
。
javascript
var oBtn = document.getElementsByTagName('button')[0];
oBtn.addEventListener('click', function(){
this.className = 'got';
this.innerHTML = '已领取';
this.removeEventListener('click', arguments.callee, false);
}, false)
严格模式下只能将事件处理函数写在外部,然后进行解绑。
javascript
var oBtn = document.getElementsByTagName('button')[0];
oBtn.addEventListener('click', test, false)
function test(){
this.className = 'got';
this.innerHTML = '已领取';
this.removeEventListener('click', test, false);
}
事件传播
如果事件的目标是Window
或其它独立对象,浏览器对这个事件的响应就是简单地调用该对象上对应的事件处理函数。如果事件目标是Document
或其它文档元素,就没有那么简单了,涉及到元素嵌套的问题。
事件冒泡
多数事件都会沿DOM
树向上"冒泡"。事件冒泡指的是事件的向上传导,当目标元素的事件被触发时,其祖先元素的相同事件也会触发,一层一层向上传导触发(这种传导是由HTML
结构决定的,和CSS样式无关),一直到Document
对象,然后到Window
对象。
以下代码中,当我们点击inner时,其外部的两个div也会触发点击事件。
css
.wrapper{
width: 300px;
height: 300px;
background-color: green;
}
.wrapper .outer{
width: 200px;
height: 200px;
background-color: red;
}
.wrapper .outer .inner{
width: 100px;
height: 100px;
background-color: orange;
}
xml
<div class="wrapper">
<div class="outer">
<div class="inner">
</div>
</div>
</div>
<script>
var wrapper = document.getElementsByClassName('wrapper')[0],
outer = wrapper.getElementsByClassName('outer')[0],
inner = outer.getElementsByClassName('inner')[0];
wrapper.addEventListener('click', function(){
console.log('wrapper');
}, false)
outer.addEventListener('click', function(){
console.log('outer');
}, false)
inner.addEventListener('click', function(){
console.log('inner');
}, false)
</script>
事件捕获
事件捕获
是一种向下传导模式,当目标元素的事件被触发时,从最外层祖先元素开始向内捕获目标元素(默认情况下这个过程不会调用事件处理函数
)。
事件传播阶段
IE
提出的是事件冒泡流(Event Bubbling)。
Netscape
提出的是事件捕获流(Event Capturing)。
W3C
综合以上两种事件流,将事件传播分为三个阶段:
- 捕获阶段 从最外层祖先元素开始向内捕获目标元素(
默认情况下这个过程不会调用事件处理函数
)。 - 目标阶段 事件捕获到目标元素,并开始调用事件处理函数。
- 冒泡阶段 事件从目标元素开始向祖先元素一层一层传递,依次触发同类型事件并调用事件处理函数。
如果需要在捕获阶段触发事件,比如addEventListener()
监听器绑定事件处理函数时,第三个参数设置true
即可。
- 当我们点击
inner
时,会发现依次打印的是wrapper
、outer
、inner
。这是因为捕获阶段是从最外层祖先元素开始向内捕获目标元素,而因为我们设置了在捕获阶段触发事件,所以是最外层祖先元素先触发事件,之后依次向下。
css
.wrapper{
width: 300px;
height: 300px;
background-color: green;
}
.wrapper .outer{
width: 200px;
height: 200px;
background-color: red;
}
.wrapper .outer .inner{
width: 100px;
height: 100px;
background-color: orange;
}
xml
<div class="wrapper">
<div class="outer">
<div class="inner">
</div>
</div>
</div>
<script>
var wrapper = document.getElementsByClassName('wrapper')[0],
outer = wrapper.getElementsByClassName('outer')[0],
inner = outer.getElementsByClassName('inner')[0];
wrapper.addEventListener('click', function(){
console.log('wrapper');
}, true)
outer.addEventListener('click', function(){
console.log('outer');
}, true)
inner.addEventListener('click', function(){
console.log('inner');
}, true)
</script>
优先级
并且还有两点要注意,首先捕获阶段的优先级是高于冒泡阶段的,其次对于事件目标本身而言,是不存在冒泡或者捕获的,它就只是按照程序执行顺序去执行。
事件对象有一个eventPhase
属性 返回事件触发的阶段,返回值1
|2
|3
分别表示捕获|目标|冒泡,我们可以利用该属性去看程序是在哪个阶段执行。
以下代码中,当我们点击inner
时,依次打印结果如下。由此可见:
- 首先是第三个参数设置为
true
的事件在捕获阶段调用事件处理函数。 - 然后是事件目标
inner
调用事件处理函数。 - 最后是第三个参数设置为
false
的事件在冒泡阶段调用事件处理函数。

javascript
wrapper.addEventListener('click', function(e){
console.log('bubblewrapper', e.eventPhase);
}, false);
outer.addEventListener('click', function(e){
console.log('bubbleouter', e.eventPhase);
}, false);
inner.addEventListener('click', function(e){
console.log('bubbleinner', e.eventPhase);
}, false);
wrapper.addEventListener('click', function(e){
console.log('wrapper', e.eventPhase);
}, true);
outer.addEventListener('click', function(e){
console.log('outer', e.eventPhase);
}, true);
inner.addEventListener('click', function(e){
console.log('inner', e.eventPhase);
}, true);
多数事件都会冒泡,但focus、blur、change、submit、reset、select等事件是没有冒泡和捕获阶段的。
事件取消
浏览器对很多用户事件都会做出响应,比如用户在一个链接上点击鼠标,浏览器会跟随该链接。再比如用户在浏览器右击会打开一些菜单。有时候我们不希望这些默认动作,就可以通过事件对象的preventDefault()
方法取消其默认动作(IE9
及以下版本浏览器不支持)。
ini
document.oncontextmenu = function(e){
var e = e || window.event;
e.preventDefault();
console.log(1);
}
事件对象还有一个returnValue
属性,默认值是true
,IE9及以下版本浏览器可以通过将该属性设为false
实现取消默认动作。
ini
document.oncontextmenu = function(e){
var e = e || window.event;
// e.preventDefault();
e.returnValue = false;
console.log(1);
}
如果我们使用设置属性的方式注册事件处理函数,还有一种兼容性更好的取消默认动作的写法,直接在函数体中写return false
,但是这种写法仅限于这种注册方式。
javascript
document.oncontextmenu = function(e){
return false;
console.log(1);
}
取消与事件关联的默认动作只是事件取消的一种情况,除此之外,还可以看调用事件对象的stopPropagation()
方法,取消事件传播。
stopPropagation()
方法是W3C
的方法规定的方法,IE
浏览器没有该方法。
xml
<div class="wrapper">
<div class="outer">
<div class="inner">
</div>
</div>
</div>
<script>
var wrapper = document.getElementsByClassName('wrapper')[0],
outer = wrapper.getElementsByClassName('outer')[0],
inner = outer.getElementsByClassName('inner')[0];
wrapper.addEventListener('click', function(){
console.log('bubblewrapper');
}, false);
outer.addEventListener('click', function(){
console.log('bubbleouter');
}, );
inner.addEventListener('click', function(e){
e.stopPropagation();
console.log('bubbleinner');
}, false);
</script>
IE
提供的是事件对象的一个属性cancelBubble
,默认值为false
,将其设为true
即可阻止事件传播。
针对兼容性我们可以对阻止事件传播进行封装:
ini
function cancelBubble(e){
var e = e || window.event;
if(e.stopPropagation){
e.stopPropagation();
}else{
e.cancelBubble = true;
}
}
事件委托与代理
事件委托(event delegation
)是指将事件处理函数给一个祖先元素注册,后代元素的同一类型事件触发时,会冒泡到祖先元素,这样就可以利用祖先元素绑定的处理函数完成对后代元素的处理,减少了事件绑定的次数,提高程序性能。
- 以下代码中我们只给
ul
注册点击事件,利用事件捕获可以让所有的li
都可以触发该事件。我们之前提到了事件对象的属性,这里可以看出target
返回的是触发点击事件的li
元素,而currentTarget
返回的是注册事件的ul
元素。
xml
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var oList = document.getElementsByTagName('ul')[0],
oLi = oList.getElementsByTagName('li'),
len = oLi.length,
item;
oList.onclick = function(e){
var e = e || window.event,
tar = e.target || e.srcElement
console.log(tar); // <li>...</li>
console.log(e.currentTarget); // <ul>...</ul>
}
</script>
- 如果我们要打印
li
元素在ul
中的索引,可以使用call
去调用indexOf()
方法。
xml
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var oList = document.getElementsByTagName('ul')[0],
oLi = oList.getElementsByTagName('li'),
len = oLi.length,
item;
oList.onclick = function(e){
var e = e || window.event,
tar = e.target || e.srcElement
var index = Array.prototype.indexOf.call(oLi, tar);
console.log(index);
}
</script>