0、参考
JavaScript高级程序设计(第三版),第13章
1、事件流
js
的事件流分为捕获和冒泡两类,目前主流的方式是使用冒泡,在特殊情况下才会启用捕获(比如这种需求:一个div
中有多个子元素,希望可以用鼠标在页面上拖动整个div
而不触发子元素的事件,就可以用事件捕获)
冒泡传播
考虑如下的DOM
结构,外面有3层div
盒子,最内部有一个button
按钮
<div class="box1">
<div class="box2">
<div class="box3">
<button class="btn">
点击按钮
</button>
</div>
</div>
</div>
为button
和div
分别注册事件,查看当点击按钮时会发生的执行结果
var box1 = document.getElementsByClassName('box1')[0];
var box2 = document.getElementsByClassName('box2')[0];
var box3 = document.getElementsByClassName('box3')[0];
var btn = document.getElementsByClassName('btn')[0];
btn.onclick = function () {
console.log('btn被触发')
};
box3.onclick = function () {
console.log('box3被触发')
};
box2.onclick = function () {
console.log('box2被触发')
};
box1.onclick = function () {
console.log('box1被触发')
};
执行结果:
btn被触发 m5-js.js:27:5
box3被触发 m5-js.js:32:5
box2被触发 m5-js.js:37:5
box1被触发 m5-js.js:41:5
事件捕获
依然考虑如下的DOM
结构
<div class="box1">
<div class="box2">
<div class="box3">
<button class="btn">
点击按钮
</button>
</div>
</div>
</div>
使用addEventListener
接口可以指定捕获或者冒泡阶段
var box1 = document.getElementsByClassName('box1')[0];
var box2 = document.getElementsByClassName('box2')[0];
var box3 = document.getElementsByClassName('box3')[0];
var btn = document.getElementsByClassName('btn')[0];
box1.addEventListener('click', function (evt) {
console.log('捕获阶段--box1被触发');
}, true);
box2.addEventListener('click', function (evt) {
console.log('捕获阶段--box2被触发');
}, true);
box3.addEventListener('click', function (evt) {
console.log('捕获阶段--box3被触发');
}, true);
btn.addEventListener('click', function (evt) {
console.log('捕获阶段--btn被触发');
}, true);
btn.addEventListener('click', function (evt) {
console.log('冒泡阶段--btn被触发');
}, false);
box1.addEventListener('click', function (evt) {
console.log('冒泡阶段--box1被触发');
}, false);
box2.addEventListener('click', function (evt) {
console.log('冒泡阶段--box2被触发');
}, false);
box3.addEventListener('click', function (evt) {
console.log('冒泡阶段--box3被触发');
}, false);
结果:
捕获阶段--box1被触发 m5-js.js:27:5
捕获阶段--box2被触发 m5-js.js:31:5
捕获阶段--box3被触发 m5-js.js:35:5
捕获阶段--btn被触发 m5-js.js:39:5
冒泡阶段--btn被触发 m5-js.js:43:5
冒泡阶段--box3被触发 m5-js.js:55:5
冒泡阶段--box2被触发 m5-js.js:51:5
冒泡阶段--box1被触发 m5-js.js:47:5
2、事件绑定--onclick接口
事件绑定有两个主要的接口,第一种接口:
button.onclick = fn
DOM 0
级,此类注册事件只会出现在冒泡阶段。
这种方式将事件触发接口(onclick)
作为元素的一个属性,值即为事件触发后执行的回调函数(fn)
。因为一个元素的一个属性只能指向一个值,故此类绑定方式中,元素只能注册一个同类的事件,旧的注册事件会被新的注册事件所覆盖,事件注销时,使用button.onclick = null
。
onclick类的接口,只能注册一个同类事件
考虑下面的DOM
结构
<button class="btn">点击按钮</button>
在按钮button
上先后绑定两个同类事件,后绑定的事件会覆盖前绑定的事件
var btn = document.getElementsByClassName('btn')[0];
btn.onclick = function (event) {
console.log('hello word')
};
btn.onclick = function () {
console.log('你好世界')
};
执行结果
你好世界 m5-js.js:32:5
onclick类的接口,使用button.onclick = null的方式注销事件
依然考虑如下的DOM
结构
<button class="btn">点击按钮</button>
在button
上绑定一个点击事件,然后启动一个定时器,在4s
之后注销此事件
var btn = document.getElementsByClassName('btn')[0];
setTimeout(function () {
btn.onclick = null;
}, 2000);
btn.onclick = function (event) {
console.log('hello word');
};
执行结果
在网页加载完毕后的2s
内,每次点击button
按钮都会打印hello world
,但是2s
之后点击就不再有反应。
hello word m5-js.js:31:5
3、事件绑定--addEventListener接口
事件绑定的第二种接口:
button.addEventListener('click', fn, false)
DOM 2
级,此类注册事件可以自选出现在捕获或者冒泡阶段。
这种事件触发接口(addEventListener)
作为元素的一个事件注册函数,参数是事件类型、回调函数、布尔值(true
为选择捕获/false
为选择冒泡,默认为false
)。此注册函数实现了类似'回调函数队列'的效果,一个元素可以注册多个同类的事件,当事件发生时,多个回调函数会依次执行。
事件注销时,使用button.removeEventListner('click', fn, false)
,参数的值必须和注册时指向的对象一致。
注意:使用removeEventListener
接口方式,无法处理匿名回调函数的事件注销。(因为匿名函数一旦创建,后续就无法获取对它的引用)
addEventListener接口,可以注册多个同类事件,发生时,依次执行回调函数
考虑如下DOM
结构
<button class="btn">点击按钮</button>
为button注册多个同类click事件,这些回调函数不会覆盖,而是会依次执行
var btn = document.getElementsByClassName('btn')[0];
btn.addEventListener('click', function (evt) {
console.log('hello func - 1');
}, false);
btn.addEventListener('click', function (evt) {
console.log('hello func - 2');
}, false);
btn.addEventListener('click', function (evt) {
console.log('hello func - 3');
}, false);
btn.addEventListener('click', function (evt) {
console.log('hello func - 4');
}, false);
执行结果
hello func - 1 m5-js.js:27:5
hello func - 2 m5-js.js:32:5
hello func - 3 m5-js.js:36:5
hello func - 4 m5-js.js:40:5
addEventListener接口无法注销以匿名函数注册的事件
考虑如下DOM
结构
<button class="btn">点击按钮</button>
button按钮注册了两个click
事件,其中一个指向有名回调函数sayHello
,一个指向匿名函数。2s
后尝试注销这两个事件,最终发现,有名函数的事件可以被注销,匿名函数的事件无法注销。
var btn = document.getElementsByClassName('btn')[0];
function sayHello() {
console.log('hello world')
}
// button注册了两个click事件,其中一个使用有名回调函数,一个使用匿名回调函数
btn.addEventListener('click', sayHello, false);
btn.addEventListener('click', function (evt) {
console.log('你好世界');
});
// 2s后尝试注销button的click事件
setTimeout(function () {
btn.removeEventListener('click', sayHello, false);
btn.removeEventListener('click', function (evt) {
console.log('你好世界');
});
}, 2000);
4、事件对象
每一次预定事件发生时,在回调函数中都可以直接引用event
对象,此对象代表了事件对象,包含事件发生的一些必要信息。事件对象中有两个关于目标的属性,event.currentTarget
代表着当前回调函数所属的对象,event.target
代表着触发事件的源对象。
考虑如下DOM
结构
<button class="btn">点击按钮</button>
为button
按钮绑定一个事件,在回调函数中可以直接调用event
对象
var btn = document.getElementsByClassName('btn')[0];
btn.onclick = function (event) {
console.log(event.currentTarget === this);
console.log(event.target === btn);
};
执行结果
true m5-js.js:27:5
true m5-js.js:28:5
this
和event.currentTarget
总是同样的引用,代表的是回调函数所属的对象。
event.target
可以获取发生事件的源头对象。
5、阻止冒泡传播和阻止默认行为
冒泡类事件流,默认情况下会将事件传播至所有父级元素,但在某些时候我们需要主动停止冒泡事件流的传播。
考虑如下DOM
结构
<div class="box1">
<button class="btn">点击按钮</button>
</div>
为button
和box1
均注册点击事件,默认情况下,button
的点击事件也会触发box1
的点击行为。但如果在button
的回调函数中执行event.stopPropagation()
就可以阻止冒泡事件的向上传播。
var box1 = document.getElementsByClassName('box1')[0];
var btn = document.getElementsByClassName('btn')[0];
box1.onclick = function () {
console.log('box1发生了click事件');
};
btn.onclick = function (event) {
console.log('btn发生了click事件');
event.stopPropagation();
};
执行结果
btn发生了click事件 m5-js.js:30:5
某些html
元素含有默认的事件行为,某些时候我们也需要主动停止默认行为的发生。
考虑如下DOM
结构
<div class="box1">
<a href="http://www.baidu.com" class="link">点击去往百度</a>
</div>
当点击a
元素的时候,会执行默认行为,即前往指定的herf
地址。但是如果使用event.preventDefault()
即可阻止默认行为的发生。如下执行结果并不会跳转到指定href
页面。
var a = document.getElementsByClassName('link')[0];
a.onclick = function (event) {
console.log('连接a发生了点击事件,取消默认行为');
event.preventDefault();
};
执行结果
连接a发生了点击事件,取消默认行为 m5-js.js:36:5
有时候,我们希望既可以阻止冒泡事件的传播,同时也可以阻止默认行为,那么就可以使用return false
。
6、事件带来的问题
第一个问题在于页面上会出现过多数量的事件,这些事件的回调函数都是需要占用内存,事件越多占用的内存也越大。某些事件有重复的情况,比如多个同类的li
标签可能绑定着同样的事件回调函数,这种事件触发的方式效比较低,还可以进一步优化,以上问题可以使用事件委托(代理)。
第二个问题在于如何妥善的处理事件的注销。比如元素在发生innerHTML
修改或者被remove
节点时,对应的回调函数也会失去引用,这些失去引用的事件可能无法被垃圾回收机制正确回收。所以在处理一个含有事件的元素时,应特别注意事件的注销操作,使用onclick = null
或者使用removeEventListener
。
7、事件委托(代理)
事件委托技术依赖于事件流冒泡传播。同类的子元素的事件发生,不应该在子元素上注册事件,而应该在共同的父元素上注册事件,由父元素来处理子元素的事件发生,此父元素即为子元素的事件委托者或者代理者。
考虑如下DOM
结构
<ul class="item-list">
<li class="item">选项1</li>
<li class="item">选项2</li>
<li class="item">选项3</li>
<li class="item">选项4</li>
<li class="item">选项5</li>
<li class="item">选项6</li>
<li class="item">选项7</li>
<li class="item">选项8</li>
<li class="item">选项9</li>
<li class="item">选项10</li>
</ul>
使用事件代理,只需要在父元素上绑定事件,所有子类元素的事件均会通过冒泡的形式传播到父元素的回调函数中处理。
var itemList = document.getElementsByClassName('item-list')[0];
itemList.onclick = function (event) {
// 利用冒泡原理来实现事件代理
var origin = event.target;
var originText = origin.innerText;
console.log('发生点击事件的元素是:', originText);
};
事件委托技术减少了多个同类子元素的重复事件注册,减少了内存的开销,事件处理的效率也更高。
此外,事件委托还有一个好处是不用关心新增子元素的事件注册操作,新增子元素只需要加入父元素即可,不必再次执行事件注册。
父元素也可以通过switch
判断子元素的方式来对不同的子元素执行相应的处理函数。
考虑如下的DOM
结构
<div class="item-list">
<button class="item">选项1</button>
<li class="item">选项2</li>
<a href="#" class="item">选项3</a>
</div>
父元素执行事件代理,同时通过switch
判断子元素的tagName
,针对不同的标签执行不同的事件处理。
var itemList = document.getElementsByClassName('item-list')[0];
itemList.onclick = function (event) {
// 利用冒泡原理来实现事件代理
var origin = event.target;
switch (origin.tagName) {
case 'BUTTON':
console.log('点击的是一个按钮');
break;
case 'LI':
console.log('点击的是一个列表项目');
break;
case 'A':
console.log('点击的是一个链接');
break;
}
};
执行结果
点击的是一个按钮 m5-js.js:26:13
点击的是一个列表项目 m5-js.js:29:13
点击的是一个链接 m5-js.js:32:13
8、模拟事件触发
在jQuery
中,提供了trigger
接口作为模拟事件的触发,此接口让我们非常方便的触发指定元素的指定事件。
考虑如下DOM
结构
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
为两个button
都注册对应的事件,在button1
的回调函数中,执行button2
的trigger
函数,触发button2
的点击事件。点击button1
后,即使在没有点击button2
的情况下,也会执行button2
的回调函数。
var $btn1 = $('#btn1');
var $btn2 = $('#btn2');
$btn1.bind('click', function () {
console.log('btn1被点击');
$btn2.trigger('click');
});
$btn2.bind('click', function () {
console.log('btn2被点击');
});
执行结果(只点击了button1
)
btn1被点击 m5-js.js:21:5
btn2被点击 m5-js.js:26:5
在原生JS中,需要使用createEvent
接口和dispatchEvent
接口实现同样的效果。
考虑如下DOM
结构
<button id="btn1">按钮1</button>
<button id="btn2">按钮2</button>
点击button1
的时候,触发button2
的click
事件
var btn1 = document.getElementById('btn1');
var btn2 = document.getElementById('btn2');
var event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0,
false, false, false, false, 0, null);
btn1.onclick = function () {
console.log('btn1被点击');
btn2.dispatchEvent(event);
};
btn2.onclick = function () {
console.log('btn2被点击');
};
执行结果(只点击了button1
)
btn1被点击 m5-js.js:24:5
btn2被点击 m5-js.js:29:5
9、总结
事件是js
中非常重要的模块,它完成了js
代码和html
代码之间的互动,通过事件可以提供丰富的高体验性用户交互,可以说通过js
实现的用户交互就是以事件作为驱动的。js
的事件模型为:目标元素+事件对象+回调函数。