likes
comments
collection
share

一文带你吃透【JS事件流】

作者站长头像
站长
· 阅读数 21

前言

本文将带大家深入了解下在面试时面试官最喜欢问的问题之一:JS事件流 。话说若在一张纸上画了一些同心圆,你把手指按住最里面圆的圆心,那么手指不仅是在最里面的圆里,还是在所有的圆里。这和JS事件流有点类似!

一、事件

在了解JS事件流之前,我们先了解下什么是JS的事件。

JS的事件是指用户在网页上进行的交互操作,例如鼠标点击、键盘输入、滚动页面等行为。当这些行为发生时,页面中与之关联的JS代码就会产生相应的响应动作,以完成网页的交互效果。

常用的事件有:

  • 鼠标事件(如:click、mouseover等)
  • 键盘事件(如:keydown、keyup等)
  • 表单事件(如:submit、reset等)
  • 文档加载事件(如:load、unload等)
  • 网页滚动事件(如:scroll、resize等)
  • 媒体事件(如:play、pause等)

二、事件流

事件流是指从页面中接受事件的顺序

话不多说,来个案例!

案例1:有3个不同颜色的div容器,最外层绿色盒子里放着红色的盒子,红色盒子里放着黑色盒子,盒子之间层层叠加。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    #app{
      width: 200px;
      height: 200px;
      background: greenyellow;
      display: flex;
      justify-content: center;
      align-items: center;
      text-align: center;
    }
    #box{
      width: 100px;
      height: 100px;
      background: red;
      color: white;
      
    }
    #title{
      width: 50px;
      height: 50px;
      background: #000;
    }
  </style>
</head>
<body>
  <div id="app">
    <div id="box">
      <div id="title"></div>
    </div>
  </div>
  <script>
  //获取每个盒子相应的DOM结构
    let app=document.getElementById('app') 
    let box=document.getElementById('box')
    let title=document.getElementById('title')
    
//给每个DOM结构绑定监听点击事件 一点击就打印值
    app.addEventListener('click',()=>{
      console.log('green');
    })
    box.addEventListener('click',(event)=>{
      
      console.log('red');
    })
   
    title.addEventListener('click',(event)=>{
      console.log('black');
    })
  </script>
</body>
</html>

效果图:

一文带你吃透【JS事件流】

可以看到点击最里面的黑色盒子,打印输出的顺序是:black -> red -> green !

一文带你吃透【JS事件流】

可以看到点击红色盒子打印输出的顺序是:red -> green !

一文带你吃透【JS事件流】

可以看到点击最外层绿色盒子,打印输出的只有:green !

这时你可能会疑惑,怎么是这样的呢?怎么每点击一个盒子,打印输出的结果还包含其他盒子的结果,而且打印顺序还不一样?别急,下面我们一起来研究下!

事件捕获

指的是事件从DOM根节点沿着DOM树往下传递,直到到达目标元素的父元素,依次触发父元素上的事件处理程序。着重于从外向内传播。

比如还是上面案例,当点击绿色盒子时,事件的传播方向为: docunment -> html -> body -> div

事件冒泡

指的是事件从目标元素开始往上冒泡,直到到达DOM根节点,依次触发所有父元素上的事件处理程序。着重于从内向外传播。

比如还是上面案例,当点击绿色盒子时,事件的传播方向为: div -> body -> html -> docunment

你可能会疑惑将这两个有什么用?那是非常有用!我们所谈到事件流就包括三个阶段

  • 事件捕获阶段
  • 目标阶段
  • 事件冒泡阶段

来张图来加深理解下!

一文带你吃透【JS事件流】

所以我们可以总结到JS事件流的具体步骤如下:

  1. window(也就是从Docunment)上往事件触发处传播,遇到注册的 捕获 事件就会触发
  2. 传播到事件触发处,触发注册的事件
  3. 事件触发处往 window 上传播,遇到注册的 冒泡 事件会触发

案例2:继续上个案例,更改js中部分代码,如下:

 <script>
    let app=document.getElementById('app')
    let box=document.getElementById('box')
    let title=document.getElementById('title')

    app.addEventListener('click',()=>{
      console.log('green');
    },true)
    box.addEventListener('click',(event)=>{
      console.log('red');
    },true)
   
    title.addEventListener('click',(event)=>{
      console.log('black');
    },true)
  </script>

效果:

一文带你吃透【JS事件流】

tip: addEventListener第3个参数传入true代表的是捕获阶段触发; 若传入false代表冒泡阶段触发,默认情况下就是false,可以省略不写。

可以看到点击里面的黑盒子打印输出的结果和我们前面输出的结果完全相反!也就是说addEventListener的第3个参数传的true——代表最后实现是由捕获阶段 触发,而不是冒泡阶段。

若将js中box的监听事件变成如下: box.addEventListener('click',(event)=>{ console.log('red'); },false)

结果:

一文带你吃透【JS事件流】

因此,我们可以得出:

任何一个事件都有这样的JS事件流,从最外层的Docunment到内层的目标的阶段,再从此目标阶段返回到最外层的Document,只不过可以利用addEventListener的第3个参数选择事件触发的阶段是什么时候。也就是在这个事件流的过程中,从捕获阶段到目标阶段再到冒泡阶段,每个事件所触发的阶段只要在这个过程就在相应阶段触发

三、阻止默认事件

有时候这种事件流机制也会带来一些麻烦,比如你只想点击某盒子内的一个按钮可以进入某页面,这时不仅点击此按钮可以进入某页面,而且点击这个盒子也能进入某页面,这就带来麻烦了!所以我们想要阻止默认事件触发(此默认事件可以是冒泡、捕获,取决事件所开启的触发阶段)。

有以下两种方法:

  • stopPropagation():终止默认事件传播到其他容器上
  • stopImmediatePropagation():终止默认事件传播到其他容器上和自己这个容器上的其他事件

继续上面案例,js代码更改如下:

<script>
    let app=document.getElementById('app')
    let box=document.getElementById('box')
    let title=document.getElementById('title')

    app.addEventListener('click',()=>{
      console.log('green');
    })
    box.addEventListener('click',(event)=>{
      
      console.log('red');
      // event.stopPropagation()
    })
   
    title.addEventListener('click',(event)=>{
      console.log('black');
      //终止事件的默认行为
      event.stopPropagation()
      // event.stopImmediatePropagation()
    })
  </script>

效果:

一文带你吃透【JS事件流】

可以看到只打印输出了 black!在黑色盒子上开启了阻止默认事件的触发,也就是阻止了冒泡。利用stopImmediatePropagation()也可以实现此效果,区别在于 stopImmediatePropagation() 还能阻止自己容器身上的其他事件。

四、事件代理(委托)

是指利用事件的冒泡原理来实现,通常委托它们的父级代为执行。

看下面案例3:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    li{
      background: #4654f9;
      font-size: 20px;
      margin: 10px;
      width: 100px;
    }
  </style>
</head>
<body>
  <ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>
</body>
</html>

如果我们要监听每一个li的行为并打印输出其html内容,你是不是会在js中这样写:

 <script>
     let li=document.querySelectorAll('li') //获取所有li
     for(let i=0;i<li.length;i++){
       li[i].addEventListener('click',()=>{ //监听li点击事件
         let e=li[i].innerHTML
         console.log(e)
       })
     }
  </script>

那要是li有很多个,这样循环会不会觉得有点麻烦。那还有其他方法吗?当然有,使用事件代理,也就是把事件委托给别人(父级)。

将js代码更改如下:

<script>
   
      let ul=document.getElementById("ul");
      ul.addEventListener("click",(e)=>{
        // console.log('123');
        console.log(e.target.innerHTML);
      })
  </script>

效果:

一文带你吃透【JS事件流】

可以发现确实可以实现我们一开始的效果!这就是利用了事件冒泡,我们每点击的li相当于点击了父级元素ul,也就会冒泡到ul,所以给ul绑定监听事件,只要点击任何一个li,都可以触发到父级ul并打印输出其内容。

总结

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,您的点赞是持续写作的动力,感谢支持。