DOM事件传递顺序

发布:elantion 日期:2018-08-06 阅读:2034 评论:0

DOM事件的传递顺序问题可能是各公司面试中最喜欢出的问题了,一来可以考验面试者对浏览器兼容性的理解,二来可以考验应聘者对底层事件的理解层度,所以如果说你还没深刻地理解这部分知识,建议接下来认真细心地读一读本篇文章。

这个问题的场景看起来其实很简单,假设有一个元素在另一个元素里面,

-----------------------------------
| element1                        |
|   -------------------------     |
|   |element2               |     |
|   -------------------------     |
|                                 |
-----------------------------------

并且都各自绑定了一个点击事件。如果一个用户点击element2元素,这时候element1element2都触发一个点击事件。但那个元素先触发点击事件?那个绑定的函数先执行?或者说,事件的触发顺序是怎样?

两种模式

如果要深刻理解浏览器的某种逻辑,最好接下来就读一读这种逻辑的演变历史。毫不意外,在浏览器刚发明的最早时期,Netscap和Microsoft采用了完全相反的模式。

Netscape认为element1最先触发,这就是现在耳熟能详的capture(捕抓)模式。
Mircosoft认为element2应该优行触发,这是我们熟知的bubbling(冒泡)模式。

Internet Explorer 6-8版本仅支持冒泡模式,Opera 7 和 Konqueror 两种模式都支持,更老一点的Opera和iCab两种都不支持。

事件捕抓

当使用事件捕抓模式时

               | |
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------

元素element1先触发事件,然后再触发element2的事件。

事件冒泡

当使用事件冒泡模式时

               / \
---------------| |-----------------
| element1     | |                |
|   -----------| |-----------     |
|   |element2  | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------

元素element2先触发事件,然后再触发element1的事件。

W3C标准模式

机智的W3C没有给任何一个厂商站队,选择了中立。W3C的事件模式是先使用“捕抓”模式,到达目标元素后再变成“冒泡”模式。

                 | |  / \
-----------------| |--| |-----------------
| element1       | |  | |                |
|   -------------| |--| |-----------     |
|   |element2    \ /  | |          |     |
|   --------------------------------     |
|        W3C event model                 |
------------------------------------------

当你需要添加一个事件时,可以使用addEventListener函数,注册为“捕抓”或“冒泡”模式,当最后一个参数为true时,就是“捕抓”模式,当是false或不设置(默认)时,就是“冒泡”模式(看来微软有先见之明)。
假设你按下面的方式注册了两个事件:

element1.addEventListener('click',doSomething2,true);
element2.addEventListener('click',doSomething,false);

当用户点击element2时会发生什么呢?


1、click事件首先会进入“捕抓”模式,从外到内,查找element2的外层元素查找是否有绑定事件,如果有就触发。

2、这个事件发现element1绑定了一个点击函数,于是触发并执行doSomething2函数。

3、到了element1,这个事件没有发现其它注册了“捕抓”模式的函数,于是它就会转为“冒泡”模式,然后就发现了element1注册了一个doSomething函数并执行了它。

4、最后,这个事件会继续向外“冒泡”,在当前的场景中没有其它事件了,所以啥也没发生。


如果反过来呢?

element1.addEventListener('click',doSomething2,false)
element2.addEventListener('click',doSomething,false)

如果用户点击element2,将会出现以下情况:

1、首先click事件会以“捕抓”模式传递,从外到内,查找element2外层是否有绑定了点击事件的元素,当前情况是没有的。

2、事件继续传递,直到目标后停止,然后立即转换成“冒泡模式”,事件从里到外继续传递,这时doSomething函数因为绑定的是“冒泡”模式,于是会被执行。

3、事件会继续向外传递,查找是否还有绑定“冒泡”模式的函数。

4、最后发现doSomething2,于是执行该函数。

兼容传统模式

除了addEventListener,我们还可以使用onClick方法来绑定事件函数。在支持W3C DOM标准的浏览器里,这个方式绑定的函数都是以“冒泡”模式绑定:

element1.onclick = doSomething2;

合理使用事件冒泡模式

现在很多开发者并没有把点击事件的捕抓和冒泡模式记在心中,通常是任由事件继续冒泡,然后触发一些不应该响应的事件。用户会感到很困惑,我明明只点击这个按钮,为什么会触发两种或以上的事情?
虽然w3c定义的来回模式有点太复杂,一不留神就触发不应该触发的事件,未来可能会有改进,但目前来说这种模式是兼容性最好,也是最合理的。

事件捕抓和冒泡一直存在

当用户点击一个元素时,事件的捕抓和冒泡模式会一直存在,例如你在整个document上绑定了一个点击函数时:

document.onclick = doSomething;
if (document.captureEvents) document.captureEvents(Event.CLICK);

点击任何元素,最终都会触发document绑定的事件函数,除非在函数里有强制停止事件传递的代码。

比较有用的默认函数

从上面可知,所有事件最终会传递到document,所以我们可以很方便地在document上绑定一个默认事件。我们接着看下面这个图:

------------------------------------
| document                         |
|   ---------------  ------------  |
|   | element1    |  | element2 |  |
|   ---------------  ------------  |
|                                  |
------------------------------------

element1.onclick = doSomething;
element2.onclick = doSomething;
document.onclick = defaultFunction;

无论点击element1还是element2,事件都会传递到document,触发执行defaultFunction。用这种方式来执行默认函数,例如记录用户点击事件,是个不错的点子。

还有,在没有drag API 时,我们常用鼠标事件来代替,这时候就必须使用类似defaultFunction来执行默认函数,模似drag move,drag off之类的行为,否则鼠标一旦移出目标,就不能准确判断拖动的行为。

截停事件传递

在大多数情况下,我们希望在执行完事件函数后停止事件继续“捕抓”或“冒泡”,以免触发一些不应该执行的函数。特别是你的DOM结构十分复杂时,截停事件传递可以避免每次触发事件后继续无意义地查找大量的元素,最后啥也没找到,节省性能资源。

要截停事件的传递其实很简单,目标浏览器是IE6-8:

window.event.cancelBubble = true;

这样就可以停止事件继续“冒泡”了(因为那些版本只有冒泡),如果目标浏览是IE9+,遵从W3C的逻辑的:

e.stopPropagation();

function doSomething (e) {
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) e.stopPropagation();
}


### currentTarget

在前面的案例里,```event``` 参数里的```target```和```srcElement```都指向用户点击的元素```element2```,无论是“捕抓”还是“冒泡”模式,它都不会改变,都是指向```element2```。

但如果按下面的方式注册事件:

element1.onclick = doSomething;
element2.onclick = doSomething;


用户点击时,```doSomething```会执行两次,但```target/srcElement```仍然指向```element2```,因为点击的目标确实是```element2```,我们好像没办法区分函数是由那个元素注册的?幸运的时,W3C定义了```currentTarget```属性,它指向的就是注册该函数的元素。

因为IE6-8没有采用W3C的模式,所以没有```currentTarget```,不过上面的情况可以使用```this```来代替,它刚好指向注册事件函数的元素。

### IE的问题

如果没办法使用上面```onclick```方法来注册函数,而使用:

element1.attachEvent('onclick',doSomething)
element2.attachEvent('onclick',doSomething)
```

这时候,this并不是指向触发函数的元素,这样就没办法获取到那个元素了,这是个非常严重的问题,开发者非常需要这个信息,幸运的是IE9实现了w3c的模式,大家以后开发时注意兼容问题。

译者后话

译文来自经典文章: https://www.quirksmode.org/js/events_order.html ,那时候还没有IE9,所以我这次翻译加上了一些现代浏览器的改进信息,希望大家在写兼性性代码时要注意哦。另外这篇文章几乎被视为面试必考的内容,所以大家要用心去记住文中的关键点,特别是“捕抓”和“冒泡”的顺序,别搞反了哦。有那儿翻译的不对,欢迎指正!