理解 DOM 事件流

DOM2 Events 规范规定事件流分为3个阶段:事件捕获、到达目标、事件冒泡。虽然 DOM2 Events 规范规定的事件流是从 document 开始,但实际上,所有浏览器都是从 window 对象就开始了。

下面的代码均在一个空白的宽高100%的页面中演示,点击的元素均为body

EventTarget

在理解事件流之前,需要先理解 EventTarget。为什么要先理解EventTarget?因为注册事件监听器(处理程序),大多通过addEventListeneronevent这两种方式。而addEventListener其实就是EventTarget.prototype上的方法。主要包括addEventListenerremoveEventListenerdispatchEvent

为什么DOM元素、window对象都可以使用addEventListener注册事件监听器,是因为他们都继承了EventTarget,包括XMLHttpRequestAudioNodeAudioContext等等。同时也都支持通过onevent注册事件监听器。

javascript 复制代码
const { addEventListener } = EventTarget.prototype

window.addEventListener === addEventListener
document.body.addEventListener === addEventListener
XMLHttpRequest.prototype.addEventListener === addEventListener
AudioNode.prototype.addEventListener === addEventListener
AudioContext.prototype.addEventListener === addEventListener

以上结果均为 true

addEventListener方法用于注册事件监听器,removeEventListener方法移除事件监听器,dispatchEvent方法触发事件监听。

javascript 复制代码
function fn() {
  console.log('事件监听')
}

document.body.addEventListener("click", fn)
const event = new Event('click')

setTimeout(function () {
  // 模拟点击事件,或 document.body.click()   
  document.body.dispatchEvent(event)
}, 1000)

setTimeout(function () {
  document.body.removeEventListener('click', fn)
}, 2000)

setTimeout(function () {
  document.body.dispatchEvent(event)
}, 3000)

1秒后打印事件监听,2秒后移除fn这个事件监听,因此3秒后不会再次打印。 其实如果从设计模式的角度理解,这就是一个简单的发布订阅模式,就像Vue中的$on$off$emitaddEventListener对应$on,也可以通过参数配置实现$once的功能,removeEventListener对应$offdispatchEvent对应$emit

事件流

addEventListener

文章开头提到,事件流分为3个阶段:事件捕获、到达目标、事件冒泡。在使用addEventListener注册事件监听器的时候,可以通过参数配置,添加到捕获还是冒泡阶段,默认注册到冒泡阶段。

javascript 复制代码
// 点击 document.body,document.body 为目标元素
window.addEventListener("click", function () {
  console.log('window clicked 事件冒泡阶段')
})

document.addEventListener("click", function () {
  console.log('document clicked 事件冒泡阶段')
})

document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件冒泡阶段')
})

window.addEventListener("click", function () {
  console.log('window clicked 事件捕获阶段')
}, true)

document.addEventListener("click", function () {
  console.log('document clicked 事件捕获阶段')
}, true)

document.body.addEventListener("click", function () {
    console.log('document.body clicked 事件捕获阶段')
}, true)

body触发点击事件时,打印结果如下:

javascript 复制代码
window clicked 事件捕获阶段
document clicked 事件捕获阶段
document.body clicked 事件捕获阶段
document.body clicked 事件冒泡阶段
document clicked 事件冒泡阶段
window clicked 事件冒泡阶段

在调用addEventListener方法,第三个参数传入了true(默认为false)时,事件监听器被注册了冒泡阶段,按照事件流执行的顺序,先是事件捕获阶段,到达document.body目标元素后,再向上冒泡,进入到事件冒泡阶段,因此打印结果如上所示。对于现在的浏览器来说,事件流是一个完整的流程,不会说只触发事件捕获,或者只触发事件冒泡,而是到达哪个阶段就执行哪个阶段注册的事件监听器。因此想在哪个阶段触发事件,就将事件监听注册到哪个阶段。

但对于到达目标阶段有点特殊。虽然 DOM2 Events 规范明确捕获阶段不命中事件目标,但现在的浏览器都会在捕获阶段在事件目标上触发事件,因此事件目标在事件捕获事件冒泡阶段都会处理事件。就像document.body clicked 事件捕获阶段所在的事件可以理解为在事件捕获阶段处理,而document.body clicked 事件冒泡阶段所在的事件在事件冒泡阶段处理。

在使用addEventListener注册事件监听时,需要注意两点:

  • 在同一阶段,传入同一个函数多次注册事件监听时,只会注册一次。
javascript 复制代码
function handleClick(){
  console.log('事件监听')
}

document.body.addEventListener("click", handleClick)

// 再次添加
document.body.addEventListener("click", handleClick)

点击一次,handleClick这个方法只会执行一次。

javascript 复制代码
function handleClick(){
  console.log('事件监听')
}

document.body.addEventListener("click", handleClick)

// 再次添加
document.body.addEventListener("click", handleClick)

// 再次添加到捕获阶段
document.body.addEventListener("click", handleClick, true)

点击一次,handleClick方法触发两次,捕获阶段一次,冒泡阶段一次。

  • addEventListener方法传入一个匿名函数时,事件监听无法被移除。
javascript 复制代码
document.body.addEventListener("click", function handleClick(){
  console.log('事件监听')
})

// error
document.body.removeEventListener('click', handleClick)

上面的代码会报错,removeEventListener方法第二个参数需要接收一个函数引用,而handleClick这个函数名,只在handleClick这个函数内部可用。

onevent

除了通过addEventListener注册事件监听以外,还可以通过元素的onevent属性添加,on加上事件类型,例如onclickonload

javascript 复制代码
document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件捕获阶段')
}, true)

document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件冒泡阶段')
})

document.body.onclick = function(){
  console.log('document.body clicked 事件冒泡阶段-onclick')
}

body触发点击事件时,打印结果如下:

javascript 复制代码
document.body clicked 事件捕获阶段
document.body clicked 事件冒泡阶段
document.body clicked 事件冒泡阶段-onclick

上面的代码中为document.body.onclick属性赋值了一个匿名函数,可以看到这个事件在事件冒泡阶段被处理。以onevent这种方式添加的事件处理程序,都是会注册到事件流的冒泡阶段。既然onevent是以属性的方式添加,那么最多也就只能添加一个,再次添加会被覆盖,可以通过设置属性为null的方式移除监听,例如document.body.onclick = null

javascript 复制代码
document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件捕获阶段')
}, true)

document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件冒泡阶段')
})

document.body.onclick = function(){
  console.log('document.body clicked 事件冒泡阶段-onclick')
}

// 事件冒泡阶段-1
document.body.addEventListener("click", function () {
  console.log('document.body clicked 事件冒泡阶段-1')
})

在上面的代码的基础上,又添加了一个事件监听事件冒泡阶段-1。 打印结果如下:

javascript 复制代码
document.body clicked 事件捕获阶段
document.body clicked 事件冒泡阶段
document.body clicked 事件冒泡阶段-onclick
document.body clicked 事件冒泡阶段-1

在同一阶段同一事件类型添加多个事件监听,在触发点击事件时,多个事件监听器执行的顺序就是添加的顺序。

Event

在 DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event的对象中,event也是传给事件监听器的唯一参数。在通过dispatchEvent方法触发事件监听时,就用到Event类生成一个event对象。不同的事件生成的事件对象会包含不同的属性和方法,但他们无疑都继承了Event。 所有的事件对象都包含一些公共属性和方法,例如:

  • type: 事件类型
  • currentTarget: 注册该事件监听器的元素
  • target: 事件目标
  • preventDefault: 阻止默认行为
  • stopPropagation: 阻止事件流继续传播
  • eventPhase: 调用当前事件监听所处的阶段:1代表捕获阶段,2代表到达目标,3代表冒泡阶段。
javascript 复制代码
document.body.addEventListener("click", function (event) {
  const { constructor: {name} } = event
  console.log(name)  // PointerEvent
  console.log(event instanceof Event) // true
})

点击后的打印结果分别为PointerEventtrue,说明click中的事件对象是PointerEvent的实例,而PointerEvent又继承了Event

javascript 复制代码
document.addEventListener("click", function ({currentTarget, target}) {
  console.log(this === currentTarget)  // true
  console.log(document === currentTarget)  // true
  console.log(document === target)  // false
})

document.body.addEventListener("click", function ({currentTarget, target}) {
  console.log(this === currentTarget)  // true
  console.log(document.body === currentTarget)  // true
  console.log(document.body === target)  // true
})

点击body时,使用普通函数注册的事件监听,在执行期间this等于currentTarget,指向注册当前事件监听的元素。而target永远指向事件目标document.body

javascript 复制代码
// 捕获
document.addEventListener("click", function (event) {
 event.stopPropagation()
 console.log('document.body clicked 事件捕获阶段')
}, true)

// 冒泡
document.body.addEventListener("click", function (event) {
  console.log('document.body clicked 事件冒泡阶段')
})

点击body时,不会打印document.body clicked 事件冒泡阶段,因为在捕获阶段就已经阻止了事件流继续传播,因此不会到达冒泡阶段。

需要注意的是,event对象只在事件监听器执行期间存在,一旦执行完毕,就会被销毁。

相关推荐
有梦想的刺儿4 分钟前
webWorker基本用法
前端·javascript·vue.js
清灵xmf1 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据1 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
334554322 小时前
element动态表头合并表格
开发语言·javascript·ecmascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家3 小时前
Vue 计算属性和监听器
前端·javascript·vue.js