一篇文章能否解决你事件监听的许多疑问

2,041 阅读6分钟

前言: 此篇文章稍长,请您用心阅读,完后我相信会解决你许多疑惑。

我们以一个 toy demo 开始.

// dom level
  -> div (onclick)
    -> p
      -> span

问题🤔: 为什么当我们点击<p>, <span> click 事件也触发了?

Bubbling(冒泡)

冒泡是事件的一种传递机制,当一个事件发生时,事件会以相反的顺序传播通过目标再父级祖先,最后以Window结束。

就拿之前的例子🌰来说,当用户点击<span>元素时,事件会从下至上(子元素->祖先)依次触发click事件。并且几乎所有的事件都会有冒泡阶段。

Capture (捕获)

我们知道在事件的传递机制中除了冒泡还有一种我们经常提到的就是捕获机制。

其实这是不完全正确的!!!,依据W3C的定义, 事件的传递分为了3阶段

  1. Capture(捕获阶段): 事件对象通过Window传播到目标的祖先父级再到自身。

  2. Target(目标阶段): 事件对象到达目标自身, 当该事件类型指定不Bubble, 则事件将在此阶段终止(稍后解释)

  3. Bubble(冒泡阶段): 事件对象以相反的顺序传播通过目标的父级祖先,并以Window结束。

image

所以当一个事件发生时标准的传递流为 捕获 -> 目标 -> 冒泡

当我们程序在主流的浏览器运行时,我们在 html 中使用 on[eventType] 或者 javascript 中使用 element. addEventListener(eventType, listener) 这些Web API像是忽略了 捕获阶段 只会运行 目标阶段 和 冒泡阶段。

问题🤔:那么这时候就产生了一个问题? 若我们在一个嵌套的Dom上分别添加事件,我们就不能改变事件的触发顺序(子元素绑定的事件会先于父元素绑定的事件触发),我们如何改变这个顺序?

EventListener Option -【 options ={} | useCapture = boolean 】

感谢 🙏 Web API 的完备性,如果你看过 addEventListener API 的定义,你会发现,声明是有第三个参数选项的,它可传入一个 boolean 或者 一个对象。

elem.addEventListener(type, listener[, options]);
elem.addEventListener(type, listener[, useCapture]);

所以当你在一个 元素 上添加对应的事件监听时, 你可以这样写:

elem.addEventListener(_, _, {capture: true})
// 对象的形式还接受更多的option:[once,...]
// 或者
elem.addEventListener(_, _, true)

他们是等价的,表示监听的回调将会在 捕获阶段 被触发。那么这样我们可以解决上面的问题。 例如:

// dom level
  -> div (onclick= log('div', true))
    -> p (onclick = log('p'))
      -> span
// 此时输出顺序为 div -> p
// 相反
 -> div (onclick= log('div'))
    -> p (onclick = log('p'))
      -> span
// 此时输出顺序为 p -> div

listener callback param - Event

首先对于listener来说,它不仅仅只能传入function还能是一个对象但这个对象必须实现Event接口(包含handleEvent(fn)属性),在此我们不对它过多解释,具体可以去参看文档.

接下来我们具体说说 listener 被调用时传入的参数 Event 或 简写为 e

e.target

// dom level
  -> div (onclick)
    -> p
      -> span

还是使用之前的例子,我们解释几个误区(这里可能存在错误🙅,希望大佬发现后不吝纠正):

⚠️误区 1 : 当<div>添加了 click 事件,是否代表 <p>, <span> 也添加了 click 事件

答: ❌, 事实上在 <div> 添加了 click 与它的子元素并没有任何关系,但是可以通过 Event 对象拿到触发事件的真正对象,这看起来就好像是 <div> 的子元素同样添加了此事件监听。记住回调是发生在添加事件监听的目标元素身上的(click 的 listener callback 是发生在 div 上的)。

⚠️误区 2: 既然事件的传递分为 捕获 -> 目标 -> 冒泡,那么为什么一次点击事件不会多次触发。

答: 从之前的 addEventListener API 我们知道 事件是可以绑定到 冒泡 或者 捕获 阶段的,当没有设置时默认是在 冒泡阶段 所以只会触发一次,那就是在对应绑定的阶段。

注意: 既然添加事件是区分阶段,那么在移除此事件时也需要明确对应阶段。

解释了上述误区,我们回过来说 e.target : 当事件触发时,e.target (read-only)的值为最深嵌套相关元素,并不一定为添加事件所对应元素。例如上诉例子,当你点击<span>时事件传递冒泡走到<div>元素其 click 事件回调被触发,而e.target 的值是 span 元素对象。

e.currentTarget

我们知道了 e.target(read-only) 并不一定为添加事件所对应元素,那么如何在回调中知道是那个元素添加的此事件监听呢? 这就是 e.currentTarget ,另外在 listener 方法内部也可以直接使用 this,它等同于(this = event.currentTarget

e.path

e.path (read-only)为一个数组eg: [span, p, div, body, html, document, Window]它表示从 e.targetwindow 所经历到元素层级。

e.stopPropagation()

我们在事件传递阶段讲 捕获阶段 的时候提到 可以提前终止不在进行冒泡阶段。 这是怎么做的了,其实可以在捕获阶段添加的监听事件回调被调用时候调用e.stopPropagation() 来阻止事件在DOM中进一步传播。 由于历史原因也可以调用 e.cancelBubble = true 来阻止事件冒泡(但不建议使用此属性,最好使用 stopPropagation 方法)

e.[other]

Event对象上还有许多属性,在这里不会全部罗列,最常用的基本上就是以上几个,其余属性还可以拿到很多信息,但部分属性可能并不是标准


以上是有关EventListener一些常识,希望大家能够不吝赐教👆

如果还有未涉及到的,欢迎提出讨论。


Why write this ?

主要是由于本人近期一次【bcz】面试的遭遇有关而发:

一是面谈中提到此话题由于我个人技艺不精很多细枝末节已经忘记,最终面试fail。主要问题在我,这也导致我想重新纪录下。

二是我尊重每个公司的观念,但不代表我认可。经历【bcz】面试后,我认为贵公司,面试存在很多问题,这给我的感觉是一家特别重视基础的公司(重视基础没有错)。拿面试官自身来说通过交流我能感觉到面试官对于知识也不够深入。汲取知识应该终保持敬畏之心,既然你们十分重视基础那么你们最好做到权威,不然在半灌水的体量下你如何来评定? 如果你让面试者有这种感觉,我只能认为你是在为了面试而面试,这不应该是应试教育。

三是写完我的自闭可能会缓解。