JS 事件委托(事件代理)那些事 —— 捕获 、冒泡

945 阅读3分钟

这几天看了 jsliang 大佬的面试的复习指南。觉得自己掌握的知识非常有限,让我危机感十足,怕自己离开现在的公司就失去竞争力。入职半年的小前端切图仔,写得不好得地方大家多多包涵指教。

食用前,请对照「github」上的例子,食用更佳。 「学习例子」

事件委托,也叫事件代理。顾名思义就是拜托别人就办事。反映在 JS 上就是,原本某个 DOM 绑定的事件委托给别的 DOM 来执行。在学习「事件委托」之前,我们先说一下「事件流」和「事件模型」

事件流

事件流,按我的理解就是,事件的传递。根据传递的方向不同一般分成捕获型和冒泡型

捕获型事件流

由父元素到子元素传递

冒泡型事件流

由子元素向父元素传递

具体什么时候执行函数是通过addEventListener()第三个参数决定的,不填默认为false,也就是冒泡时执行,为true,捕获时执行。

事件模型

事件模型和事件流的关系,说一下自己的理解。事件模型其实是不同浏览器之间制定的处理标准,不同事件模型对应的事件流也存在差异。

事件模型主要有三种模型:

  • 原始事件模型 (没有事件流)
  • DOM2事件模型 (W3C规范的标准事件模型)
  • IE事件模型 (IE浏览器独有)

这里主要说一下「DOM2 事件模型」,也是现代浏览器的标准。其他两种事件模型,可以参考文章下方第二个链接。

「DOM2事件模型」事件流分为三个阶段:

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

还是以前面的 demo 为例,点击 li 时,事件就开始传递。首先是捕获阶段(document-->html-->body-->div-->ul-->li),然后是处于目标阶段,也就是到达你点击的 li ,处于捕获和冒泡的中间,最后是冒泡阶段(li-->ul-->div-->--body-->html-->document)

看到这里大概就清楚捕获和冒泡是怎么一回事了。我们来看看他们的使用场景

冒泡

我们在开发的过程中一定开发过「下拉加载列表」的功能,一般情况下,我们的做法都是循环遍历出列表的数据,并逐条添加点击事件。

例如:

	<div class="list-container">
		<ul class="list">
		</ul>
	</div>
	<button class="btn">添加列表项</button>
	<script>
		let listContainer = document.getElementsByClassName("list-container")[0] //div
		let list = document.getElementsByClassName("list")[0] //ul
		let listItem = document.querySelector(".list-item")
		let btn = document.querySelector(".btn") //button
		let doSome = (text) => {
			console.log(text)
		}
		let dataList = ["1", "2", "3", "4", "5"]

		let addDOM = (text) => {
			let newDOM = document.createElement("li")
			newDOM.classList.add("list-item")
			newDOM.addEventListener("click", () => {
				doSome(text)
			})
			newDOM.innerHTML = text
			list.appendChild(newDOM)
		}

		dataList.forEach((x, i) => {
			addDOM(x)
		})

		btn.addEventListener("click", () => {
			dataList.push(dataList.length + 1)
			addDOM(dataList[dataList.length - 1])
		})

这样会带来一个问题,就是会占用大量的内存,从而影响网页的性能。数据比较少的时候还好说,但一般情况下,「下拉加载列表」的数据量都不会太小我们可以利用「冒泡事件流」的特性来,解决这个问题。我们只要在「ul」或者「div」中添加点击事件,就可以实现上一个例子的功能。我们参考下面的例子:

    <div class="list-container">
		<ul class="list">
		</ul>
	</div>
	<button class="btn">添加列表项</button>
	
	<script>
		let list = document.getElementsByClassName("list")[0] //ul
		let listItem = document.querySelector(".list-item")
		let btn = document.querySelector(".btn") //button
		let dataList = ["1", "2", "3", "4", "5"]

		let addDOM = (text) => {
			let newDOM = document.createElement("li")
			newDOM.classList.add("list-item")
			//添加自定义属性,可以存接口的需要的属性值,例如:查看该项数据详细的ID
			newDOM.setAttribute("data-text", text) 
			newDOM.innerHTML = text
			list.appendChild(newDOM)
		}

		dataList.forEach((x, i) => {
			addDOM(x)
		})

		list.addEventListener("click", (e) => {
			console.log(e.target.dataset.text) //获取自定义属性 data-text 的值
		})

		btn.addEventListener("click", () => {
			dataList.push(dataList.length + 1)
			addDOM(dataList[dataList.length - 1])
		})
	</script>

这就是事件委托。这里值得注意的是,当你需要确定判断,自己点击的是哪个元素或者修改目标元素的样式的时候,可以使用「event.target」里面的属性来实现。上面的例子就演示了通过「e.target.dataset」来获取自定义属性 data-text 的值。

阻止冒泡

冒泡虽然在上述问题中表现很优异,但不要被骗了,他可能是个「朱朝阳」。是个坏孩纸。

假设现在父元素和子元素中都绑定了点击事件,这时候我们点击子元素的时候,父元素的点击事件也跟着触发了。一般情况下我们都只是希望目标元素的事件触发而已。这时候就轮到「阻止冒泡」出场了。我们可以使用「event.stopPropagation()」来阻止事件冒泡。

<div class="list-container">
		<ul class="list">
			<li class="list-item">1</li>
		</ul>
	</div>
	<script>
		let listContainer = document.getElementsByClassName("list-container")[0] //div
		let list = document.getElementsByClassName("list")[0] //ul
		let listItem = document.querySelector(".list-item") //l

		let doSome = (text) => {
			console.log(text)
		}
		listItem.addEventListener("click", (e) => {
			console.log("这个打印信息只是说明使用「e」和「event」是等价的,可以相互使用", e === event) //true 自定义的参数「e」和「event」对象引用是相同的。
			e.stopPropagation() //阻止事件传递
			doSome('li————最先执行')
		})
		list.addEventListener("click", (e) => {
			console.log("——————ul没有阻止冒泡——————")
			doSome('ul————接着执行')
		})
		listContainer.addEventListener("click", () => {
			doSome('div————最后执行')
		})
	</script>

捕获

非常惭愧,自己在开发的过程中没有利用过「捕获」来实现功能。在网上搜了一下,也没有找到能说出个所以然的文章。如果你知道有哪些经典的例子,请在评论区里留言,供大家学习学习,先说声谢谢。

来自阿怪的个人博客:kite1874.com/19/

补充

以下事件不支持冒泡

  • onblur
  • onfocus
  • onmouseenter
  • onmouseleave

参考文章: