12-事件捕获与事件冒泡
18.11 事件捕获与事件冒泡
由于HTML是层级结构,这使得可以在多个地方处理事件。比如,当一个button被点击时,不仅button本身可以处理这个点击事件,它的父亲、父亲的父亲,也可以处理这个事件。那么问题来了,“这些元素是按照什么顺序来处理事件呢?”
这里有两种可能性。一个是从最外层的祖先节点开始,称为事件捕获(capturing)。在本例中,botton是 <div id="content"> 的孩子节点,而它又是
另一个选择是从触发事件的元素开始,然后按照HTML的层级结构向上走,这样所有的祖先都有机会处理事件。这称作事件冒泡(bubbling)。
为了支持这两种方式,在HTML5中,事件的传播是从处理器捕获事件开始(从最外层的祖先节点,一直到目标元素),然后才是事件冒泡,也就是从目标元素到最外层的祖先节点。
任何一个处理器都可以通过以下三件事中的一件来影响(或阻止)其他处理器的调用。第一件同时也是最常见的,是已经见过的 preventDefault ,它可以取消事件。取消的事件会继续传播,但是它们的 defaultPrevented 属性会被置成true。浏览器内置的事件处理器都会遵从 defaultPrevented 属性,不执行任何操作。而自己编写的事件处理器则可以(通常也会这么做)选择性地忽略这个属性。第二件事是调用stopPropagation,时间传播到当前元素之后,将不会继续传播(所有绑定到当前元素上的处理器都会被调用,但绑定到其他元素上的处理器不会被调用)。最后一件事,也是重头戏:调用 stopImmediatePropagation 会阻止所有还未调用的处理器被调用(即使这些处理器被绑定在当前元素上)。
为了演示这些功能,我们来看看下面的HTML:
<!doctype html>
<html>
<head>
<title>Event Propagation</title>
<meta charset="utf-8">
</head>
<body>
<div>
<button>Click Me!</button>
</div>
<script>
// 创建一个事件处理器并返回这个处理器
function logEvent(handlerName, type, cancel,
stop, stopImmediate) {
// 这才是真正的事件处理器
return function(evt) {
if(cancel) evt.preventDefault();
if(stop) evt.stopPropagation();
if(stopImmediate) evt.stopImmediatePropagation();
console.log(`${type}: ${handlerName}` +
(evt.defaultPrevented ? ' (canceled)' : ''));
}
}
// 给元素上添加一个logger事件
function addEventLogger(elt, type, action) {
const capture = type === 'capture';
elt.addEventListener('click',
logEvent(elt.tagName, type, action==='cancel',
action==='stop', action==='stop!'), capture);
}
const body = document.querySelector('body');
const div = document.querySelector('div');
const button = document.querySelector('button');
addEventLogger(body, 'capture');
addEventLogger(body, 'bubble');
addEventLogger(div, 'capture');
addEventLogger(div, 'bubble');
addEventLogger(button, 'capture');
addEventLogger(button, 'bubble');
</script>
</body>
</html>
点击button,会在console中打印以下内容:
capture: BODY
capture: DIV
capture: BUTTON
bubble: BUTTON
bubble: DIV
bubble: BODY
从这个例子中可以清楚地看到事件传播的顺序:先是事件捕获,然后是事件冒泡。注意,在产生事件的元素上,处理器被调用的顺序就是它们被添加的顺序,不论它们是捕获事件还是传播事件(如果把捕获处理器和冒泡处理器的添加顺序颠倒一下,那冒泡处理器将会在捕获处理器之前被调用)。
下面来看一下如果取消事件传播会发生什么。修改前面的例子,取消 <div> 上的捕获事件传播。
addEventLogger(body, 'capture');
addEventLogger(body, 'bubble');
addEventLogger(div, 'capture', 'cancel');
addEventLogger(div, 'bubble');
addEventLogger(button, 'capture');
addEventLogger(button, 'bubble');
可以看到传播还在继续,但是事件已经被标记为取消:
capture: BODY
capture: DIV (canceled)
capture: BUTTON (canceled)
bubble: BUTTON (canceled)
bubble: DIV (canceled)
bubble: BODY (canceled)
现在停止
addEventLogger(body, 'capture');
addEventLogger(body, 'bubble');
addEventLogger(div, 'capture', 'cancel');
addEventLogger(div, 'bubble');
addEventLogger(button, 'capture', 'stop');
addEventLogger(button, 'bubble');
看,事件在传播到 <button> 元素后就停止传播了。但是,即使事件捕获先行触发,并且停止了传播, <button> 上的冒泡事件依然会触发。不过 <div> 和 <body> 元素却接收不到它们的冒泡事件:
capture: BODY
capture: DIV (canceled)
capture: BUTTON (canceled)
bubble: BUTTON (canceled)
最后,立即停止
addEventLogger(body, 'capture');
addEventLogger(body, 'bubble');
addEventLogger(div, 'capture', 'cancel');
addEventLogger(div, 'bubble');
addEventLogger(button, 'capture', 'stop!');
addEventLogger(button, 'bubble');
至此可以看到事件传播被
capture: BODY
capture: DIV (canceled)
capture: BUTTON (canceled)
addEventListener取代了以前用来添加event的方法:使用“on”属性添加event。例如,在元素elt上添加一个点击处理器可以用 `elt.onclick = function(evt) {/*handler */}` 。用这种方式注册事件处理器的最大缺点是一次只能注册一个处理器。
讲了这么多,并不意味着在开发中需要经常提前知道事件的传播并控制它,事件传播这个话题经常会让初学者觉得困惑。不过,牢牢掌握事件传播机制可以将高级程序员跟一般的开发人员区分开来。
在jQuery的事件监听器中,直接在处理器中返回false与调用 `stopPropagation` 是等价的,这是jQuery的约定,但这种快捷方式不能在DOM API中使用。
addEventListener取代了以前用来添加event的方法:使用“on”属性添加event。例如,在元素elt上添加一个点击处理器可以用 `elt.onclick = function(evt) {/*handler */}` 。用这种方式注册事件处理器的最大缺点是一次只能注册一个处理器。