JavaScript中的冒泡与捕获

首先来看一个例子来明白什么是冒泡和捕获,来看下面的一个html结构

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <style>
  7. .outer {
  8. width: 200px;
  9. height: 200px;
  10. background-color: black;
  11. margin: 100px auto;
  12. }
  13. .inner {
  14. width: 100px;
  15. height: 100px;
  16. background-color: greenyellow;
  17. }
  18. </style>
  19. </head>
  20. <body>
  21. <div class="outer">
  22. <div class="inner"></div>
  23. </div>
  24. </body>
  25. </html>

就是一个大盒子里面套着一个小盒子,为两个盒子设置了背景颜色以作区分,如下
JavaScript中的冒泡与捕获 - 图1
现在为二者都添加一个点击方法

  1. <script>
  2. let outer = document.querySelector('.outer');
  3. let inner = document.querySelector('.inner');
  4. outer.addEventListener('click', (e) => {
  5. console.log('outer被点击了');
  6. });
  7. inner.addEventListener('click', (e) => {
  8. console.log('inner被点击了');
  9. });
  10. </script>

现在如果点击里面的盒子inner,那么outer的点击事件会不会触发,因为按道理也算是点击了outer的区域,所以outer的点击事件应该被触发。现在问题又来了,是先触发inner还是先触发outer的点击事件呢? 按照二者触发顺序的不同分为捕获和冒泡。

现在点击绿色的小盒子,看看输出是什么
JavaScript中的冒泡与捕获 - 图2
当我们点击里面的盒子即inner时,触发了它的点击事件,随后触发了outer的点击事件,这样触发子元素事件之后触发父元素事件的行为就叫做冒泡;捕获就是随之相反了,先处理outer,然后处理inner的事件。

要实现捕获的效果,首先我们为addEventListener方法的第三个参数设置为true,如下

  1. outer.addEventListener('click', (e) => {
  2. console.log('outer被点击了');
  3. }, true);
  4. inner.addEventListener('click', (e) => {
  5. console.log('inner被点击了');
  6. }, true);

这时我们在点击里面的盒子
JavaScript中的冒泡与捕获 - 图3
这时是outer的点击事件先被执行,然后是inner的点击事件被执行。

冒泡和捕获的出现是因为以前的两大浏览器厂商Netscape和Microsoft对事件模型处理方法,Microsoft采取的从目标元素(比如点击innerinner就是目标元素)开始,按DOM树向上冒泡;而Netscape采取的是相反的原则,即从顶部元素开始,直到事件目标元素。通过上面的例子可以知道,可以通过设置addEventListener方法的第三个参数可以设置是冒泡还是捕获,当设置为true时,是捕获,当设置为false时,是冒泡,默认是false。

现在考虑一个比较复杂的DOM结构,如下

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. <style>
  7. .one {
  8. width: 200px;
  9. height: 200px;
  10. background-color: black;
  11. margin: 100px auto;
  12. }
  13. .two {
  14. width: 150px;
  15. height: 150px;
  16. background-color: aliceblue;
  17. }
  18. .three {
  19. width: 100px;
  20. height: 100px;
  21. background-color: greenyellow;
  22. }
  23. .four {
  24. width: 50px;
  25. height: 50px;
  26. background-color: blueviolet;
  27. }
  28. </style>
  29. </head>
  30. <body>
  31. <div class="one">
  32. <div class="two">
  33. <div class="three">
  34. <div class="four"></div>
  35. </div>
  36. </div>
  37. </div>
  38. <script>
  39. let one = document.querySelector('.one');
  40. let two = document.querySelector('.two');
  41. let three = document.querySelector('.three');
  42. let four = document.querySelector('.four');
  43. one.addEventListener('click', (e) => {
  44. console.log('one被点击了');
  45. }, true);
  46. two.addEventListener('click', (e) => {
  47. console.log('two被点击了');
  48. }, false);
  49. three.addEventListener('click', (e) => {
  50. console.log('three被点击了');
  51. }, true);
  52. four.addEventListener('click', (e) => {
  53. console.log('four被点击了');
  54. }, false);
  55. </script>
  56. </body>
  57. </html>

上面四个盒子套在一起,我们为onethree设定为捕获模式,为twofour设定为冒泡模式,如果我们点击four,这时的输出会是什么呢?
JavaScript中的冒泡与捕获 - 图4
我们观察到输出的顺序为one -> three -> four -> two,代码是怎么执行的呢? 首先事件处理器会从顶部开始即one(严格的说是从window),一直到目标元素,在这个路径中,如果遇到设置为捕获模式的则执行,碰到冒泡模式的则跳过,达到目标元素后,开始转换为冒泡模式,向上冒泡到one,在这个路径中,如果碰到设置为冒泡模式的则执行,否则跳过。

现在我们来看看上面的执行流程:

  • 首先从one开始向下捕获,one设置为捕获模式,执行
  • 遇到twotwo设置为冒泡模式,不执行跳过
  • 遇到threethree设置为捕获模式,执行
  • 遇到four,到达目标元素,执行(此时不管four是冒泡还是捕获都没有关系,都会执行的)
  • 接着转变为冒泡模式,遇到threethree为捕获模式,跳过
  • 遇到twotwo为冒泡模式,执行
  • 遇到oneone为捕获模式,不执行,此时已经到达顶部,结束

通过上面的流程,不难知道输出的顺序为什么是one -> three -> four -> two

在父元素上代理事件

我们来看一个运用冒泡的小例子,假设有这个一个DOM结构

  1. <ul>
  2. <li>元素</li>
  3. <li>元素</li>
  4. <li>元素</li>
  5. <li>元素</li>
  6. <li>元素</li>
  7. <li>元素</li>
  8. <li>元素</li>
  9. <li>元素</li>
  10. <li>元素</li>
  11. <li>元素</li>
  12. </ul>

我们希望当点击li标签时,将li标签里面的文字变为红色,所以很有可能你会写出这样的代码

  1. document.querySelectorAll("ul li").forEach(item => {
  2. item.addEventListener('click', (e) => {
  3. e.target.style.color = 'red';
  4. })
  5. })

这样写当然能达到效果,但是如果ul下面有成千上万个li,这样写未免性能太低,我们可以利用冒泡的特性,为ul绑定点击事件,如下

  1. document.querySelector('ul').addEventListener('click', (e) => {
  2. e.target.style.color = 'red';
  3. });

这样不管ul下面有多少个li都没有关系。

解释:

这里可能有人不太懂e.target是什么,e.target是指触发点击事件的元素,而不是ul,因为我们点击的是li标签,所以这里的e.target是被点击的li元素。如果想在addEventListener里面访问ul元素,可以使用this。

阻止事件冒泡

有的时候我们不希望事件有冒泡操作,我们可以通过event对象的stopPropagation方法来阻止事件冒泡,以文章开头的innerouter为例(outerinner都设置为冒泡模式),我们给inner的addEventListener修改为

  1. inner.addEventListener('click', (e) => {
  2. e.stopPropagation();
  3. console.log('inner被点击了');
  4. });

这时我们点击inner,这时只有inner的点击事件被执行了
JavaScript中的冒泡与捕获 - 图5