阅读 765

JS中的事件顺序(事件捕获与冒泡)

一. 问题

如果一个元素和它的祖先元素注册了同一类型的事件函数(例如点击等), 那么当事件发生时事件函数调用的顺序是什么呢?

比如, 考虑如下嵌套的元素:

-----------------------------------
| outer                           |
|   -------------------------     |
|   |inner                  |     |
|   -------------------------     |
|                                 |
-----------------------------------
复制代码

两个元素都有onclick的处理函数. 如果用户点击了inner, innerouter上的事件处理函数都会被调用. 但谁先谁后呢?

二. 两个模型

在刚刚过去的那些糟糕年代, Netscape和M$对此有不同的看法.

Netscape认为outer上的处理函数应该先被执行. 这被称作event capturing.

M$则认为inner上的处理函数具有执行优先权. 这被叫做event bubbling.

事件捕获(event capturing)

               | |
---------------| |-----------------
| outer        | |                |
|   -----------| |-----------     |
|   |inner     \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------
复制代码

outer上的事件处理器先触发, 然后是inner上的.

事件冒泡(event bubbling)

               / \
---------------| |-----------------
| outer        | |                |
|   -----------| |-----------     |
|   |inner     | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------
复制代码

与事件捕获相反, 当使用事件冒泡时, inner上的事件处理器先被触发, 其后是outer上面的.

三. W3C 模型

W3C标准则取其折中方案. W3C事件模型中发生的任何事件, 先(从其祖先元素document)开始一路向下捕获, 直到达到目标元素, 其后再次从目标元素开始冒泡.

          1. 先从上往下捕获
                  |
                 | |  / \
-----------------| |--| |-----------------
| outer          | |  | |                |
|   -------------| |--| |-----------     |
|   |   inner    \ /  | |          |     |
|   |                  |           |     |
|   |   2. 到达目标元素后从下往上冒泡|     |
|   --------------------------------     |
|        W3C event model                 |
------------------------------------------
复制代码

而你作为开发者, 可以决定事件处理器是注册在捕获或者是冒泡阶段. 如果addEventListener的最后一个参数是true, 那么处理函数将在捕获阶段被触发; 否则(false), 会在冒泡阶段被触发.

例如如下的代码:

var selector = document.querySelector.bind(document);
selector('div.outer').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'outer clicked! '
}, true)
selector('div.inner').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'inner clicked! '
}, false)
document.addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'document clicked! '
}, true)
复制代码

当点击inner元素时, 如下事情发生了:

  1. 点击事件开始于捕获阶段. 在此阶段, 浏览器会在inner的所有祖先元素上查找点击事件处理函数(从document开始).

  2. 结果找到了2个, 分别在documentouter上面, 而且这两个事件处理函数的useCapture选项为true, 说明它们是被注册在捕获阶段的. 于是, documentouter的点击处理函数被执行了.

  3. 继续向下寻找, 直到达到inner元素本身. 捕获阶段就此结束. 此时进入冒泡阶段, inner上的事件处理器得到执行.

  4. 事件命中目标元素后开始向上冒泡, 一路查找是否有注册了冒泡阶段的祖先元素上的事件处理器. 由于没有找到, 因此什么也没发生.

最后的结果是:

// log
document clicked! outer clicked! inner clicked!
复制代码

如果我们把祖先元素的事件处理器注册在冒泡阶段的话(addEventListeneruseCapture选项为false):

var selector = document.querySelector.bind(document);
selector('div.outer').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'outer clicked! '
    console.log(e);
}, false)
selector('div.inner').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'inner clicked! '
    console.log(e);
}, false)
document.addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'document clicked! '
}, false)
复制代码

结果则是:

// log
inner clicked! outer clicked! document clicked!
复制代码

传统模型 element.onclick = function(){}将被注册在冒泡阶段.

四. 事件冒泡的应用

例如: 当点击时的默认函数.

如果在document上注册一个点击函数:

document.addEventlistener('click', (e) => {}, false)

那么任何元素上的点击事件最后都会冒泡到这个事件处理器上并触发函数 - 除非前面的事件处理函数阻止了冒泡(e.stopPropagation(), 在这种情况下事件不会继续向上冒泡)

注意: e.stopPropagation()只能阻止事件在冒泡阶段的向上传播.如果被点击元素的祖先元素有注册在捕获阶段的事件处理器:

ancestorElem.addEventListner('click', (e) => {
// do something...
}, true)
复制代码

那么该祖先元素上的事件处理器照样会在捕获阶段被触发.

因此, 你可以在document上设置这么一个处理函数, 当页面上的任何元素被点击时, 这个处理函数就被会触发. 一个实用的例子就是下拉菜单: 当点击文档上除下拉菜单本身时任意一处时, 下拉菜单会被隐藏.

在冒泡或者捕获阶段, e.currentTarget指向当前事件处理函数所附着的元素. 你也可以用事件处理函数内的this取而代之.

事件委托:

利用事件冒泡的特性,将里层的事件委托给外层事件,根据event对象的属性进行事件委托,改善性能。 使用事件委托能够避免对特定的每个节点添加事件监听器;事件监听器是被添加到它们的父元素上,事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件。

举个例子:鼠标放到li上对应的li背景变灰。

<ul>
    <li>item1</li>
    <li>item2</li>
    <li>item3</li>
    <li>item4</li>
    <li>item5</li>
    <li>item6</li>
</ul>
复制代码

利用事件冒泡实现:

$("ul").on("mouseover",function(e){
    $(e.target).css("background-color","#ddd").siblings().css("background-color","white");
})
复制代码

当然也可以直接给所有li都绑上事件,例如:        

$("li").on("mouseover",function(){
    $(this).css("background-color","#ddd").siblings().css("background-color","white");
})
复制代码

从代码简洁程度上,两者是相若的。但是,前者少了一个遍历所有li节点的操作,所以在性能上肯定是更优的。

还有就是,如果我们在绑定事件完成后,页面又动态的加载了一些元素:

$("<li>item7</li>").appendTo("ul");
复制代码

这时候,第二种方案,由于绑定事件的时候item7还不存在,所以还要给它再绑定一次事件。而利用冒泡方案由于是给ul绑的事件,无需再次绑定。

五. M$模型的麻烦

在M$模型中, 没有对e.currentTarget的支持, 更糟糕的是, this也不指向当前的HTML元素。

(文章纯属个人备忘记录用途,部分引用来自网上加上个人理解整理。欢迎转载,请注明出处。如对你有帮助,请随意打赏。)

关注下面的标签,发现更多相似文章
评论