DOM事件机制
一、事件机制
事件是在编程时系统内发生的动作或者发生的事情,系统会在事件出现的时候触发某种信号并且会提供一个自动加载某种动作的机制(来自MDN)。
每个事件都有事件处理器(有时也叫事件监听器),也就是触发事件时运行的代码块。严格来说事件监听器监听事件是否发生,然后事件处理器对事件做出反应。
二、DOM事件模型和事件流
DOM事件模型分为捕获和冒泡。一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段。
(1)捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
(2)目标阶段:真正的目标节点正在处理事件的阶段;
(3)冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
三、事件捕获
事件发生时,在捕获阶段,事件从窗口向下通过DOM树传播到目标节点,即从最外层元素(祖先元素)触发事件响应函数,逐级往下,直到目标元素。
如果目标元素的任何祖先(即父、祖父等)和目标本身具有针对该类型事件专门注册的捕获事件侦听器,则这些侦听器将在捕获阶段执行。
eg:demo1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JavaScript Event Capturing Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showTagName() {
alert("Capturing: "+ this.tagName);
console.log()
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showTagName, true);
}
</script>
</body>
</html>
其触发结果是:
// 点击DIV时
Capturing: DIV
// 点击P时
Capturing: DIV
Capturing: P
// 点击a时
Capturing: DIV
Capturing: P
Capturing: A
可以看出,触发具有父元素的元素的事件时,都是从最外层开始逐级往下传播事件的。
target.addEventListener(type, listener, useCapture(optional));
target: 事件目标type:事件类型listener:事件触发响应函数useCapture(optional):一个布尔值,指示在将该类型的事件分配给DOM树中它下面的任何EventTarget之前,是否将其分配给注册的侦听器,捕获阶段为true,没有值时默认为false
四、事件冒泡在事件冒泡阶段,正好相反。
事件冒泡模式流程:事件发生时,先触发目标元素(最直接元素)的事件响应函数,然后触发其父元素的事件响应函数,并逐级上溯到祖先元素。
在现代浏览器中,默认所有事件处理程序都注册在冒泡阶段。(场景)
eg:demo2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JavaScript Event Bubbling Demo</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('Bubbling: ' + this.tagName)">DIV
<p onclick="alert('Bubbling: ' + this.tagName)">P
<a href="#" onclick="alert('Bubbling: ' + this.tagName)">A</a>
</p>
</div>
</body>
</html>
所有浏览器都支持事件冒泡,它适用于所有处理程序,不管它们是如何注册的,例如使用onclick或addEventListener()(除非它被注册为捕获事件监听器)。这就是为什么事件传播这个术语经常被用作事件冒泡的同义词。
五、访问目标元素
目标元素是生成事件的DOM节点。
event.target可以作为目标元素访问,其不会在事件传播阶段改变。
此外,this关键字表示当前元素(即具有当前正在运行的处理程序的元素)。
六、阻止事件传播
在嵌套的元素中,并且每个元素都有事件处理程序时,当单击内部元素,所有处理程序都将同时执行,因为事件会出现在DOM树中。
为了防止这种情况,可以使用event.stopPropagation()方法停止事件使DOM树冒泡。
在以下示例中,如果单击子元素,则不会执行父元素上的click事件监听器。
eg:demo3
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Demo3</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div id="wrap">DIV
<p class="hint">P
<a href="#">A</a>
</p>
</div>
<script>
function showAlert(event) {
alert("You clicked: "+ this.tagName);
event.stopPropagation();
}
var elems = document.querySelectorAll("div, p, a");
for(let elem of elems) {
elem.addEventListener("click", showAlert);
}
</script>
</body>
</html>
另外,甚至可以使用stopImmediatePropagation()方法阻止执行附加到同一事件类型的相同元素的其他任何侦听器。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Demo3</title>
<style type="text/css">
div, p, a{
padding: 15px 30px;
display: block;
border: 2px solid #000;
background: #fff;
}
</style>
</head>
<body>
<div onclick="alert('You clicked: ' + this.tagName)">DIV
<p onclick="alert('You clicked: ' + this.tagName)">P
<a href="#" id="link">A</a>
</p>
</div>
<script>
function sayHi() {
alert("Hi, sueRimn!");
event.stopImmediatePropagation();
}
function sayHello(){
alert("Hello World!");
}
// 将多个事件处理程序附加到超链接
var link = document.getElementById("link");
link.addEventListener("click", sayHi);
link.addEventListener("click", sayHello);
</script>
</body>
</html>
如果将多个侦听器附加到同一事件类型的同一元素上,则按它们被添加的顺序执行。但是,如果任何侦听器调用``event.stopImmediatePropagation() 方法,则不会执行剩余其他侦听器。
比如上方代码,按照添加顺序,应该先执行sayHi(),然后执行sayHello(),但是在执行sayHi()时调用了event.stopImmediatePropagation() 方法,所以sayHello()不会被执行了。
七、阻止默认事件
有些事件具有与之关联的默认操作。例如点击一个链接浏览器带你到链接的目标,点击一个表单提交按钮浏览器提交表单等等。
可以使用事件对象的preventDefault()方法来防止此类默认操作。但是,阻止默认操作并不会停止事件传播,事件像往常一样继续传播到DOM树。
九、事件代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
1.优点
- 减少内存消耗,提高性能
假设有一个列表,列表之中有大量的列表项,我们需要在点击每个列表项的时候响应一个事件
// 例4
<ul id="list">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
......
<li>item n</li>
</ul>
如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。借助事件代理,我们只需要给父容器ul绑定方法即可,这样不管点击的是哪一个后代元素,都会根据冒泡传播的传递机制,把容器的click行为触发,然后把对应的方法执行,根据事件源,我们可以知道点击的是谁,从而完成不同的事。
在很多时候,我们需要通过用户操作动态的增删列表项元素,如果一开始给每个子元素绑定事件,那么在列表发生变化时,就需要重新给新增的元素绑定事件,给即将删去的元素解绑事件,如果用事件代理就会省去很多这样麻烦。
2.如何实现
接下来我们来实现上例中父层元素 #list 下的 li 元素的事件委托到它的父层元素上:
// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
// 兼容性处理
var event = e || window.event;
var target = event.target || event.srcElement;
// 判断是否匹配目标元素
if (target.nodeName.toLocaleLowerCase === 'li') {
console.log('the content is: ', target.innerHTML);
}
});
八、小结
目前主流的浏览器事件传播流程都遵循DOM的事件流机制,即先由外到内捕获流程,然后是由内到外的冒泡流程。大多数情况下,都只在冒泡阶段响应事件,捕获事件很少使用。虽然``stopPropagation`可以阻断事件流,但是通常不推荐使用,只有在确实有必要时使用。