带你深度解析Js中的事件机制

537 阅读5分钟

从问题谈起

最近在写一个仿饿了么的Vue项目,实现了店铺头部部分,点击这个头部会显示店铺的基本信息:

1.png

2.png

当想要关闭时,点击下面关闭的图标发现这个页面关闭不了。为了快速发现问题,我询问了ChatGPT,它给了我三个可能出现的问题,我一个一个去排查,发现不是前两个的问题,那就很有可能是第三个问题:

3.png

为了彻底弄懂,我查了一些资料,接下来就一起来看一下吧。

Js事件流

是什么?

Js中事件执行的整个过程称之为事件流,分为三个阶段:事件捕获阶段,处于目标阶段、事件冒泡阶段。

事件捕获:当鼠标点击或者触发dom事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。看下面图示:

4.png

处于目标阶段:传播到事件触发处,触发注册的事件。

事件冒泡阶段:与事件捕获阶段相反,由内到外进行事件传播,直到根节点。如下图所示:

5.png

有点难理解?我们直接看下面的代码演示。

代码演示

我在页面上定义了三个div容器并设置了一定的样式(下面代码无Css),分别拿到了他们的Dom结构进行事件监听:

<body>
    <div id="app">div1
        <div id="box">div2
            <div id="title">div3
            </div>
        </div>
    </div>

    <script>
        let app = document.getElementById('app')
        let box = document.getElementById('box')
        let title = document.getElementById('title')
        app.addEventListener('click', () => {
            console.log('div1');
        })
        box.addEventListener('click', () => {
            console.log('div2');
        })
        title.addEventListener('click', () => {
            console.log('div3');

        })

    </script>
</body>

此时页面效果为:

6.png

当点击最里面的容器(即黑色容器)代码打印顺序为:div3div2div1。此时用Js事件机制解释为:当点击黑色容器时,开始进行事件捕获,Js事件流从window上往事件触发处传播,遇到注册的捕获事件就会触发;但是捕获阶段默认是不处理的(addEventListener第三个参数默认是false),紧接着传播到事件触发处,触发注册的事件,打印div3,然后进行冒泡阶段从事件触发处 往window上传播,遇到注册的冒泡事件会触发,打印div2,再打印div1

用图示表示Js事件流传播过程:

7.png

那我把第二个容器的监听点击事件传入第三个参数true,不让它默认为false即:

 box.addEventListener('click', () => {
            console.log('div2');
        },true)

这时点击黑色容器时打印结果是什么?

我们分析一下,第三个参数为true时,相当于第二个容器的捕获阶段会执行事件,冒泡阶段反而不会,所以打印顺序为:div2,div3,div1

阻止默认事件

上面讲到的捕获事件和冒泡事件都是默认事件,阻止默认事件的发生在我们日常开发中需要经常使用,就比如文章一开始的问题。那怎么阻止默认事件呢?

stopPropagation()

此方法可以终止默认事件传播到其他容器上。

用法

我们还是用上面代码为例,在第三个容器监听事件中加入这个方法(用事件参数调用):

title.addEventListener('click', (event) => {
            console.log('div3');
            event.stopPropagation()
        })

同样点击黑色容器,此时打印结果只有div3,因为这个方法阻止了事件流的传播,事件流到div3盒子打印了div3,接着就中断了,不会进行冒泡阶段了,用图表示为:

8.png

stopImmediatePropagation()

这个方法和上面方法相似,也是终止默认事件传播到其他容器上;同时它也可以终止自己这个容器的其他事件。

用法

还是用上面的例子,这次在第三个容器监听事件中添加两个事件,看下面代码:

 title.addEventListener('click', (event) => {
            console.log('div3');
            event.stopImmediatePropagation()
        })
 title.addEventListener('click', () => {
            console.log('_div3');
        })

同样点击黑色容器,此时打印结果也只有div3,这个方法终止了默认事件,同时也终止了这个容器的其他事件,如果是stopPropagation()方法会有两个打印结果,也就是它不会终止这个容器的其他事件,这是这两个方法的区别。

事件委托

我们借助Js存在的事件流特性能在我们开发中起大作用;如果有多个DOM节点需要监听事件的情况下,给每个DOM绑定监听函数,会极大的影响页面的性能,我们可以通过事件委托来进行优化,事件委托利用的就是冒泡的原理。

举个例子:如果我们在页面上5个li都添加点击事件,你可能会这样干:

let li = document.getElementsByTagName('li')

    for (let i = 0; i < li.length; i++) {
        li[i].addEventListener('click', () => {
            console.log(li[i].innerHTML);
        })

    }

这样写是没有问题的,但是我们用事件委托来完成,当点击li的时候通过冒泡是不是在ul上注册的事件也会触发,所以我们只需要在ul上绑定一个点击事件,同时事件参数中有一个target,记录是哪一个li冒泡,可以返回触发事件的元素。这样做相当于li上的事委托到了ul上来完成,看下面代码:

  let ul = document.getElementById('ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target.innerText);
    })

利用事件委托显然是更加优雅的,每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。

学“废”了

看到这里,相信大家对Js中的事件机制有了一定的了解。回到前面一开始项目中的问题,当我们点击关闭图标时,由于事件的冒泡机制,触发了关闭事件,紧接着又触发了打开事件,所以页面关闭不了。在Vue中这个问题很好解决,我们只需要在点击事件后面添加stop,就可以阻止冒泡, @click.stop。本篇文章到这就结束了,创作不易,点个免费的赞是对作者最大的鼓舞ヾ(◍°∇°◍)ノ゙。