DOM事件模型&事件委托

事件模型

一个事件发生后,会在子元素和父元素之间传播(propagation),这种传播分成三阶段

  • 从上到下的捕获阶段(capture phase):从window对象传导到目标节点(上层传到底层)
  • 目标阶段(target phase):在目标节点上触发
  • 从下到上的冒泡阶段(bubbling phase):从目标节点传导回window对象(从底层传回上层)

dom.png

冒泡(bubbling phase)

当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素的处理程序,一直向上到其他祖先

假设我们有 3 层嵌套 FORM > DIV > P,它们各自拥有一个处理程序:

1
2
3
4
5
6
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>

1
2
3
4
5
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>

当你点击内部的p标签会首先运行 onclick:

  1. 在该 p标签 上的。
  2. 然后是外部 div标签 上的。
  3. 然后是外部 form标签 上的。
  4. 以此类推,直到最后的 document 对象。

点击 p标签,那么我们将看到 3 个 alert:p → div → form。

这个过程称为冒泡,因为事件是从内部冒泡到所有父级元素,就像水里的气泡一样

event.target 与 event.currentTarget

  • event.target 是引发事件的嵌套层级最深的目标元素,也就是用户操作的元素
  • event.currentTarget 是程序员监听的元素

🌰举个例子🌰

如果我们有一个处理程序 form.onclick,它可以“捕获”表单内的所有点击。无论用户点击发生在哪里,它都会冒泡到 form 并运行处理程序。

在 form.onclick 处理程序中:

  • this(=event.currentTarget)是
    元素,因为处理程序在它上面运行(程序员监听的元素)
  • event.target 是表单中实际被用户点击的元素
  • event.target 等于 event.currentTarget的情况:当点击事件发生在 form 元素上时

阻止冒泡

用于停止冒泡的方法是event.stopPropagation(),下面这个例子中,点击button元素,body.onclick不会生效

1
2
3
<body onclick="alert(`the bubbling doesn't reach here`)">
<button onclick="event.stopPropagation()">Click me</button>
</body>

捕获(capture phase)

1
div.addEventListener('click', fn, bool)
  • bool 不传 或为 falsy(默认值),则在 冒泡阶段 设置处理程序fn(一般默认情况就是冒泡)
  • bool 为 true,则在 捕获阶段 设置处理程序fn(true 是 {capture: true} 的简写形式)

在现实世界中,当事故发生时,当地警方会最先做出反应。他们最了解发生这件事的地方。然后,如果需要,上级主管部门再进行处理。

事件处理程序也是如此。特定于 td元素 的处理程序可能恰好适合于该 td元素,这个处理程序知道关于该元素的所有信息。所以该处理程序应该首先获得机会。然后,它的直接父元素也了解相关上下文,但了解的内容会少一些,以此类推,直到处理一般性概念并运行最后一个处理程序的最顶部的元素为止。

事件委托

  • 如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上
  • 这种方式是基于事件传播过程中,逐层冒泡总能被祖先节点捕获
  • 在处理程序中,我们获取 event.target 以查看事件实际发生的位置并进行处理

事件委托的优点

  1. 节省监听数,也就是节省了内存

当你想要监听100个按钮的点击事件,在每个按钮上添加onclick事件显然是笨拙的做法。用事件委托我们可以监听100个按钮的祖先元素,等到冒泡阶段,再判断 event.target 是不是这100个按钮中的一个

1
2
3
4
5
6
7
div.addEventListener('click', (e) => {
const t = event.target
if(t.tagName.toLowerCase() === 'button'){
console.log('button被点击了')
console.log('button data-id是'+ t.dataset.id)
}
})

  1. 可以动态监听元素

当我们想要监听一个暂时还不存在的元素时,可以监听它的祖先,在用户点击了后看是不是我想要监听的元素

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout(()=>{
const button = document.createElement('button')
button.textContent = 'click me'
div1.appendChild(button)
},1000)

div1.addEventListener('click',(e)=>{
const t = e.target
if(t.targetName.toLowerCase() === 'button'){
console.log('button被click')
}
})

手写可拖拽的 div

预览
注意点:

  1. clientX 是鼠标在当前窗口下的水平坐标
  2. deltaX = 鼠标移动后的位置 - 初始位置
  3. css里要加上position
0%