一. 问题
如果一个元素和它的祖先元素注册了同一类型的事件函数(例如点击等), 那么当事件发生时事件函数调用的顺序是什么呢?
比如, 考虑如下嵌套的元素:
- -----------------------------------
- | outer |
- | ------------------------- |
- | |inner | |
- | ------------------------- |
- | |
- -----------------------------------
两个元素都有 onclick 的处理函数. 如果用户点击了 inner, inner 和 outer 上的事件处理函数都会被调用. 但谁先谁后呢?
二. 两个模型
在刚刚过去的那些糟糕年代, 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 元素时, 如下事情发生了:
点击事件开始于捕获阶段. 在此阶段, 浏览器会在 inner 的所有祖先元素上查找点击事件处理函数(从 document 开始).
结果找到了 2 个, 分别在 document 和 outer 上面, 而且这两个事件处理函数的 useCapture 选项为 true, 说明它们是被注册在捕获阶段的. 于是, document 和 outer 的点击处理函数被执行了.
继续向下寻找, 直到达到 inner 元素本身. 捕获阶段就此结束. 此时进入冒泡阶段, inner 上的事件处理器得到执行.
事件命中目标元素后开始向上冒泡, 一路查找是否有注册了冒泡阶段的祖先元素上的事件处理器. 由于没有找到, 因此什么也没发生.
最后的结果是:
- // log
- document clicked! outer clicked! inner clicked!
如果我们把祖先元素的事件处理器注册在冒泡阶段的话(addEventListener 的 useCapture 选项为 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 元素.
来源: https://juejin.im/post/5aab87f9f265da23994e47dc