浅谈js的事件机制,事件冒泡、事件捕获、事件代理(事件委托)

206 阅读6分钟

引言

简单来说,当用户或者你操作网页的DOM时(鼠标点击、鼠标移动等),会触发一些事件,为处理用户的交互,而做出一些响应。因此你需要了解事件的传播方式(捕获、冒泡)和使用事件委托来提高性能。

事件简述

JavaScript中,事件是以事件流的形式存在的,事件流的顺序分为:捕获和冒泡。阶段是:捕获、模板、冒泡。简单来说:

  • 事件的顺序
    • 捕获:外 > 内
    • 冒泡:内 > 外
  • 事件的阶段
    • 1.捕获阶段
    • 2.目标阶段
    • 3.冒泡阶段

添加事件监听器

JavaScript中,可以使用addEventListener方法,来为元素绑定事件。

element.addEventListener(event, handler, useCapture);
  • event 绑定的事件类型
  • handler 事件触发时要执行的函数
  • useCapture 事件处理的阶段,可选值。false(默认,冒泡阶段) true(捕获阶段)

移除事件监听器

要移除添加在元素上的事件监听器,需要调用removeEventListener()方法,并传递事件的类型event和之前添加的方法的handler,如下所示:

element.removeEventListener(eventType, handlerName);
  • eventType 绑定的事件类型
  • handlerName 之前添加的事件触发时要执行的函数名

如果没有写函数名,就无法移除事件监听器。

事件冒泡

先讲讲事件冒泡,因为addEventListener的默认处理时机就是冒泡阶段。也就是说,如果有3个盒子逐层嵌套(box1蓝色 box2绿色 box3粉色),当你点击了粉色时,那么会打印的是:box3粉色、box2绿色、box1蓝色(由内到外)。

image.png

<div class="box1">
    <div class="box2">
        <div class="box3"></div>
    </div>
</div>
const ebox1 = document.querySelector('.box1')
const ebox2 = document.querySelector('.box2')
const ebox3 = document.querySelector('.box3')

ebox1.addEventListener('click', function() {
  console.log('box1蓝色')
})

ebox2.addEventListener('click', function() {
  console.log('box2绿色')
})

ebox3.addEventListener('click', function() {
  console.log('box3粉色')
})

那么,如果改变事件的处理顺序,为蓝色粉色盒子添加,第三个参数为true,代码如下。当你再次点击粉色盒子,猜猜看打印的顺序是什么?

点击查看答案 box1蓝色、box3粉色、box2绿色
...

ebox1.addEventListener('click', function() {
  console.log('box1蓝色')
}, true)

ebox2.addEventListener('click', function() {
  console.log('box2绿色')
})

ebox3.addEventListener('click', function() {
  console.log('box3粉色')
}, true)

因为添加了第三个参数,改变了其处理的顺序。分析一下,原先的触发顺序是由内到外,现在box1box3的处理顺序被变为了由外到内。所以当你点击box3粉色盒子,box1蓝色在最外面,会先打印;其次再打印box3粉色,最后再打印box2绿色

  • box3粉色,捕获,外>内
  • box2绿色,冒泡,内>外
  • box1蓝色,捕获,外>内

事件捕获

事件捕获,其触发的顺序与事件冒泡的相反,在上面已经举过类似的例子,就不再赘述。

阻止事件传播

阻止事件传播,常用的有两种方法:

  • event.stopPropagation() 阻止事件传播,不会阻止同一元素上的其他的事件处理程。
  • event.stopImmediatePropagation() 阻止事件传播,阻止同一元素上的其他的事件处理程。

event.stopPropagation()

如果一个元素上绑定了多个事件处理程序,调用event.stopPropagation()方法只会阻止事件往高级元素传播,而不会阻止同一元素上的其他事件处理程序被触发。

image.png

假设你为box3粉色盒子添加了event.stopPropagation()方法,猜猜打印的是什么?

点击查看答案 box1蓝色、box3粉色、box3粉色 222
点击查看分析 因为,box1蓝色,box3粉色,是捕获阶段触发(外>内);box2绿色是冒泡阶段触发(内>外);当你点击粉色的时候,首先会触发蓝色,然后再触发粉色(绑定了两个事件,就会打印两条数据)。至于绿色不触发,是由于为粉色添加了阻止事件传播,所以不会向外层继续传播事件,也就是说绿色的是不会触发的。
...

ebox1.addEventListener('click', function() {
  console.log('box1蓝色')
}, true)

ebox2.addEventListener('click', function() {
  console.log('box2绿色')
})

ebox3.addEventListener('click', function(event) {
  console.log('box3粉色')
  event.stopPropagation()
}, true)

ebox3.addEventListener('click', function(event) {
  console.log('box3粉色 222')
}, true)

event.stopImmediatePropagation()

如果一个元素上绑定了多个事件处理程序,调用event.stopImmediatePropagation()方法会立即停止事件传播,并且不会触发同一元素上的其他事件处理程序。

点击查看答案 box1蓝色、box3粉色
点击查看分析 因为,box1蓝色,box3粉色,是捕获阶段触发(外>内);box2绿色是冒泡阶段触发(内>外);当你点击粉色的时候,首先会触发蓝色,然后再触发粉色(虽然绑定了两个事件,但是由于使用了event.stopImmediatePropagation()方法,所以就不会触发粉色盒子的其他事件了)。至于绿色不触发,是由于为粉色添加了阻止事件传播,所以不会向外层继续传播事件,也就是说绿色的是不会触发的。
...

ebox1.addEventListener('click', function() {
  console.log('box1蓝色')
}, true)

ebox2.addEventListener('click', function() {
  console.log('box2绿色')
})

ebox3.addEventListener('click', function(event) {
  console.log('box3粉色')
  event.stopImmediatePropagation()
}, true)

ebox3.addEventListener('click', function(event) {
  console.log('box3粉色 222')
}, true)

事件代理(事件委托)

从字面意思理解,委托就是把事情交给别人处理。在JavaScript中,事件委托就是把子元素的事件交给父元素处理。

举个例子

现在要为每一个li添加一个事件,假设li有100个,你就需要为每一个li添加一个事件,这样会占用100个内存。因此,如果使用事件委托的话,可以利用事件的冒泡机制,为ul绑定一个事件,那么点击任意一个li的时候,都会将事件触发到父元素ul上。

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    ...
</ul>

代码展示

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    ...
</ul>

可以使用event.target获取到点击的元素,event.target.innerHTML获取到点击的元素的内容。 如下代码,当你点击li时,会添加一个红色的背景,再次点击,会将背景变为白色。

const ulE = document.querySelector('#ul')

ulE.addEventListener('click', function (event) {
  console.log('ulE event', event.target, event.target.innerHTML)
  const target = event.target
  if (target.style.backgroundColor !== 'red') {
    target.style.backgroundColor = 'red'
  } else {
    target.style.backgroundColor = '#fff'
  }
})

因此,使用事件代理/委托可以提高性能,减少注册的事件。

附录

常见的事件

  • 键盘事件

    • keydown 按下键盘上的一个键时
    • keyup 释放键盘上的一个键时
  • 鼠标事件

    • click 单击事件
    • dblclick 双击事件
    • mousedown 鼠标按钮被按下
    • mouseup 鼠标按钮被释放
    • mousemove 鼠标光标在元素上移动时
    • mouseover 鼠标光标移动到某个元素上时,类似 CSS 的 hover
    • mouseout 鼠标光标移出元素边界时
    • contextmenu 打开上下文菜单时,例如单击鼠标右键时
  • 其他事件

    • submit 表单被提交时
    • DOMContentLoaded DOM内容完全加载时

参考文献