本文深入解析W3C DOM Level 2事件模型的捕获与冒泡阶段,指出Opera浏览器正确实现了规范中目标元素不触发捕获事件的特性,而Firefox和Safari存在偏差。同时对比IE的Microsoft Model,重点分析attachEvent导致的重复绑定及this指向window的问题,建议谨慎使用事件捕获。
W3C DOM事件捕获机制与浏览器差异
在W3C DOM Level 2事件模型规范中,事件在DOM树中的传播过程(从根节点到目标节点)被明确划分为两个阶段:捕获(Capture)和冒泡(Bubbling)。

(from W3C)
若要在支持W3C事件模型的浏览器中创建捕获事件,只需将addEventListener的第三个参数设置为true即可。例如:
代码如下:
document.getElementById('foo').addEventListener('click',function(){alert('Hello, world!');},true); 前阵子为了深入理解事件捕获机制,我在Firefox 2、Windows平台下的Safari 3以及Opera 9上进行了实验(由于IE不支持事件捕获,故未包含在内)。实验原理如下图所示:

ID为div1和div2的两个元素均绑定了捕获阶段的事件处理函数,具体设置如下:
当点击#div1(蓝色区域)时,预期会弹出提示框显示“div1”
当点击#div2(黄色区域)时,预期会先弹出“div1”,随后弹出“div2”。这是因为在事件捕获阶段,事件是从根元素向下传播的,#div1作为#div2的父元素,其绑定的click事件自然会在#div2的事件之前执行。
然而,上述预期仅在Firefox 2和Windows平台下的Safari 3中成立。在Opera 9中,实际行为却有所不同:
当点击#div1(蓝色区域)时,没有任何反应
当点击#div2(黄色区域)时,会弹出“div1”,随后不再有任何反应
由此可见,在Opera 9中,目标元素(TargetElement)的click事件并未被执行。经Realazy(orz…)指点,查阅了《Event capture explained》一文,发现Opera的实现才是符合标准的。文中提到:
The DOM spec states that capturing events should not fire on target, because the idea of a capturing event is to detect events before they reach their targets. Because of bugs in Gecko and Safari, web content that is tested mostly with Firefox or other Gecko-based browsers sometimes expects capturing listeners to fire on target. Such content will fail in Opera 7, 8 and current releases of 9 because of its correct implementation of the standard.
大意是:DOM规范明确指出,捕获型事件不应在目标元素上触发,因为捕获事件的初衷是在事件到达目标之前进行监测。Firefox和Safari的实现均存在bug。
再参考W3C DOM Events规范中的原文:
A capturing EventListener will not be triggered by events dispatched directly to the EventTarget upon which it is registered.
因此,整个事件传播的执行顺序应为:
父元素中所有的捕获型事件(如果有)自上而下执行
目标元素的冒泡型事件(如果有)
父元素中所有的冒泡型事件(如果有)自下而上执行
了解这些后,或许暂时避免使用事件捕获更为稳妥。
IE的高级事件处理模型的问题
IE attachEvent的重复绑定与this指向陷阱
重复绑定
IE没有addEventListener,但提供了自己的attachEvent,即Microsoft Model。两者实现基本一致,区别在于attachEvent的第一个参数(事件类型)需加“on”前缀,而addEventListener不需要;此外,由于attachEvent不支持事件捕获,因此没有第三个参数。
然而,attachEvent还有一个致命问题:重复绑定事件。(这一知识点来自ppk on JavaScript)
一个例子:
代码如下:
function sayHello(){
alert('Hello, world!');
}
// W3C Model
$('div1').addEventListener('click', sayHello, false);
$('div1').addEventListener('click', sayHello, false);
// Microsoft Model
$('div1').attachEvent('onclick', sayHello);
$('div1').attachEvent('onclick', sayHello); 在W3C模型中,相同事件处理函数的绑定会被忽略,即第二个$('div1').addEventListener('click', sayHello, false);会被忽略。
而在Microsoft模型中,第二个$('div1').attachEvent('onclick', sayHello);同样会被执行,因此点击#div1时,提示框会弹出两次。更糟糕的是,在detachEvent时,也需要调用两次detachEvent才能彻底从#div1的click事件中移除sayHello。
为什么不继续使用alertID()了?
这是因为IE事件模型的另一个缺陷。在alertID中,使用了this关键字来指代绑定了该事件处理函数的元素。在W3C模型中,alertID中的this指代#div1或#div2。
但在Microsoft模型中,由于缺乏对this的支持,this.id变为undefined,因为此时this指代的是window对象。