事件代理(Event Delegation)是前端开发中一种常用的事件处理技术。它是基于事件冒泡机制的一种优化方案,通常用于处理大量子元素的事件,尤其是当这些子元素是动态生成的时候。简单来说就是原本绑定在子元素上的事件不绑定在子元素上,而是一起交给父元素进行代理触发。
先来个情景,如果一个ul中有多个子元素li,希望点击任意一个li,都能在控制台输出这个li中的内容。
最容易想到的当然是给每个li分别绑定一个点击事件,但这需要给每个li取一个id来分别进行绑定,这么写既不优雅,并且在有大量li或li是动态生成的情况时,可能会造成大量代码的冗余,导致性能问题和代码难以维护的问题。实际工作中遇到这种情况时,如果不想卷铺盖走人最好不要轻易尝试(doge)。
那么,有没有更好的方法呢?当然是有的,我们直接获取到所有的li形成一个数组,再去遍历这个li数组去给每个li绑定点击事件。
但是,我们知道,在js中,函数是存在堆内存中的,如果有n个li,就要在堆中存入n个回调函数,这样太浪费内存,性能不好。
还能怎么优化呢?我们先来了解一下要用到的基础知识。
JS中的事件流机制
JavaScript 中的事件流机制描述了事件是如何在DOM树中传播的。事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
给出三个嵌套的div,分别为grand、parent、child,我们分别给这三个div写点简单的样式。再绑定一个点击事件,输出对应的内容。
点击最小的子元素child,我们观察控制台的输出可以发现,三个元素上的点击事件都被触发了,且顺序为child、parent、grand;同样的,点击parent元素,会触发自身的点击事件和它的父元素grand的点击事件,顺序为parent、grand。点击grand只触发了grand上的点击事件。
为什么点击子元素还能触发父元素身上绑定的事件呢?这时,有两个可能的猜测:1.事件会从子元素传到父元素,2.这里的child元素也在父元素的区域内,点击child也相当于点击了parent。
严谨起见,我们修改一下子元素的大小,为了方便辨识又给子元素加上了标记。
点击parent范围内的child部分和范围外的child部分,输出的结果依然是一样的,说明点击事件的传播与范围是没有关系的,只和父元素和子元素的关系有关。这个传播机制,就是js中的事件流。
JS中的事件流分为捕获阶段、目标阶段和冒泡阶段
- 捕获阶段 --- 事件从window处往目标处传播(此阶段默认并不触发事件)
- 目标阶段 --- 在目标处触发事件
- 冒泡阶段 --- 事件从目标处往window传播(js中的事件默认在冒泡阶段触发)
e.stopPropagation()和e.stopImmediatePropagation()
一栏文章,根据前面提到的事件流,如果我们点击作为子元素的点赞按钮,那么在冒泡阶段应该也势必会触发父元素文章栏的点击事件,跳转到这篇文章详情页。但实际上并没有,我们可以只点赞而不跳转页面。
实现这样的效果,就需要我们阻止冒泡,让事件流在目标阶段触发了目标事件后就结束,不再继续执行冒泡。在事件对象e上,有一个stopPropagation()方法,这个方法可以阻止事件流的继续传播。
在child的点击事件中调用e.stopPropagation(),就能在目标阶段触发目标事件后,阻止继续冒泡,那么就会只触发child的点击事件而不触发父元素上的事件。
e.stopPropagation()不仅可以阻止事件在冒泡阶段继续传播,还可以阻止事件在捕获阶段进一步传播。
通过设置addEventListener()
的第三个参数为true或false,我们可以决定事件在捕获阶段是否触发(默认为false),为true则在捕获阶段触发,为false则在冒泡阶段触发。
点击parent,事件在捕获阶段执行,先执行grand上的点击事件,再传播到parent,所以先输出grand再输出parent;点击child,由于事件流在parent执行完后停止,没有传播到child上,所以也是先输出grand再输出parent。
顺带一提,还有一个和e.stopPropagation()长得很像的方法:e.stopImmediatePropagation(),它的作用是阻止当前阶段(捕获或冒泡)内的其他事件处理程序执行。它不仅可以阻止事件流传播,还能阻止当前元素绑定的其他事件的执行。
parent元素上不仅绑定了输出"parent1"事件,还绑定了输出"parent2"事件,点击parent,事件流在捕获阶段依次触发输出grand、parent1后,阻止了当前阶段的其他事件处理,因此不会再触发parent上绑定的输出parent2事件了。
特殊地,某些事件类型天生就不支持事件流,这意味着它们不会在DOM结构中传播。例如:
onload
:此事件在文档或图像加载完成后触发,但不会冒泡或捕获。onerror
:当脚本执行错误或者资源加载失败时触发,同样没有冒泡或捕获阶段。onfocus
/onblur
:这些事件发生在元素获得或失去焦点时,通常与表单元素相关联,它们也不会触发事件流。onchange
:当表单域的内容改变时触发,也没有事件流。
js中的事件代理
回到最初的问题,通过js的事件流,我们就可以把元素身上需要响应的事件,委托给它的父元素(或者祖先元素)处理。就像寝室里每个人的外卖都到了,所有人都去拿太浪费时间了,不如喊寝室长一声义父,让寝室长统一去拿(doge)。
这种方法的好处在于,无论子元素有多少个,甚至是在运行时动态添加的子元素,都可以通过父元素上的事件监听器来处理事件。这可以减少事件监听器的数量,提高性能,并且使得代码更加灵活和可维护。
原文链接:https://juejin.cn/post/7409196648037400617