一、背景
假设有一个HTML代码片段如下:
<div id="div"> <input type="button" value="点击测试"></input> </div>
如果我们同时给 div 元素和 input 元素注册 click 事件,当点击 input 元素时,哪个事件先执行?
要回答这个问题,先得明白:
HTML文档是层级嵌套结构,页面元素处理事件时,总是最外层元素最先捕获到事件,再层层向下传递给子元素。这称为事件捕获阶段。
最里层子元素接收到事件后,再层层向上传递给父元素。这是事件的冒泡阶段。
两个阶段都可以处理我们感兴趣的事件,这就是下文介绍的事件注册模式。
二、事件注册模式
1、内联模式
<input type="button" value="点击测试" onlcick="alert('click on btn.')"></input>
这是最古老的事件注册模式,事件处理函数作为 HTML 元素属性被添加,由网景(Netscape)发明,除 IE3 for Mac 外所有浏览器都支持。不推荐使用。
2、传统模式
随着 DHTML 的出现,我们处理 web 页面的方式被彻底改变,事件注册模式必须变得灵活多样,以适应这种改变。于是浏览器厂商推出了新的事件注册模式。由于网景最先推出该模式,从而成为事实上的标准,后续包括 IE 在内的所有浏览器都支持该标准。写法如下:
// 添加事件处理操作
element.onclick = function(){ alert("click event!"); };
// 移除事件处理操作
element.onclick = null;
3、高级模式
为了解决传统模式的不足,微软推出了自己的事件注册模式,同时 W3C 也在 DOM2 规范中给出了注册模式。于是就有两种注册模式。
1)W3C模式
// 添加一个事件处理函数 element.addEventListener("click", function(){ //xxx }, false); // 添加两个事件处理函数 element.addEventListener("click", doOne, false); element.addEventListener("click", doTwo, false); // 移除事件处理函数 element.removeEventListener("click", doOne, false);
W3C 模式接收 3 个参数,分别为事件类型、事件处理函数和事件处理阶段。说明如下:
- 事件处理阶段参数,true=事件捕获,false=事件冒泡,你不确定的话,直接 false。
- 事件处理函数中的 this 关键字即为元素自身。
- 在 DOM3 规范中,新增了eventListenerList 属性,记录当前注册到该元素上的事件处理函数。
- 即使使用 removeEventListener 方法移除一个未绑定的事件操作,也不会报错。
2)微软模式
// 添加一个事件操作 element.attachEvent("onclick", function(){ //do something }); // 添加两个事件操作 element.attachEvent("onclick", function(){ // do something }); element.attachEvent("onclick", handler); // 移除事件操作 element.detach("onclick", handler);
不足:
》事件只能冒泡,不能捕获;
》事件处理函数是被引用,而不是复制,所以 this 关键字总是 window,完全没用。
以上两个不足导致的后果是,当事件冒泡时,我们无法知道当前是哪个元素在处理该事件。
三、回到问题
在 W3C 标准未出之前,网景采用“事件捕获”方式处理事件顺序,微软则采用“事件冒泡”方式。这里仅对 W3C 标准做说明。
W3C 标准兼容两种方式,将事件处理过程分作两个阶段:事件捕获阶段和事件冒泡阶段。如下图所示,注册在事件捕获阶段的事件处理函数在捕获阶段执行,注册在冒泡阶段的处理函数在冒泡阶段执行。
/
----------| 捕 |------| 冒 |----------- | div | 获 | | 泡 | | | --------| 阶 |------| 阶 |-----------| | input | 段 | | 段 | |
| / | |--------------------------------------
当采用传统模式注册事件处理函数时,实际使用的是事件冒泡处理方式。
所以如果采用传统模式注册事件,则点击input元素时,先执行绑定在input上的click事件,再执行绑定在div上的click事件;
如果使用W3C标准,则根据第三个参数决定:true:捕获阶段处理,先执行div上的事件,再执行input上的事件;false则顺序相反。
在低版本 IE 浏览器下,只支持事件冒泡模式。
三、阻止事件传播
在使用传统模式和W3C标准时,事件处理函数默认接受一个事件对象参数。
如果不想让注册在父元素上的事件被子元素捕获,或者子元素的事件不想冒泡到父元素,则可以调用事件对象的 stopPropagation 方法。
比如 input 是 div 的子元素,所以当点击 input 时,会同时触发注册在 div 和 input 上的事件。
如果只想触发注册在 div 上的元素,则应在捕获阶段处理 div 上的事件,并在 div 的事件处理函数中调用 event 的 stopPropagation 方法,阻止事件向子元素传播。
如果希望点击 input 时只触发 input 的 click 事件,则应在冒泡阶段处理 div 上的事件,并在 input 的事件处理函数中调用 event 的 stopPropagation 方法,阻止事件冒泡到父元素。
参考链接:Early event handlers,Traditional event registration model,Advanced event registration models,Event order。