0%

DOM事件和事件委托

DOM 事件、事件委托和默认动作

事件捕获和冒泡

捕获和冒泡的历史问题

1
2
3
4
5
6
7
8
9
10
<div class="grandfather">
<div class="father">
<div class="son">
文字
</div>
</div>
</div>
<script>
// 分别给三个 div 添加事件监听 fnGrandfather/fnFather/fnSon
</script>

IE 和 Netscape 都认为点击文字,也算点击了 son,father,grandfather。但在调用顺序上,IE5 认为先调用 fnGrandFather,

而 Netscape 认为先调用 fnSon。最后 W3C 在 2002 发布 DOM Level 2 Events Specification 文档规定:

  • 浏览器应该同时支持两种调用顺序,开发者自己选择函数在捕获阶段执行还是冒泡阶段执行
  • 首先按爷爷=>爸爸=>儿子的顺序看有没有函数监听(捕获阶段)
  • 然后按儿子=>爸爸=>爷爷顺序看有没有函数监听(冒泡阶段)
  • 有监听函数就调用,并提供事件信息(event对象),没有就跳过

捕获和冒泡阶段

1
father.addEventListener('click',fn,bool) // bool 不传或为 falsy 就会在冒泡阶段调用 fn

image-20210411235943451

一种特殊情况
  • 只有一个 div 被监听
  • 分别在捕获阶段和冒泡阶段监听 click 事件
  • 用户点击的元素就是开发者监听的

在这种情况下谁先监听谁先执行,并不一定是在捕获阶段监听的函数先执行。

取消冒泡

1
e.stopPropagation()  //中断冒泡,浏览器不再向上通知

target 和 currentTarget

  • e.target 是用户操作的元素
  • e.currentTarget 是当前设置监听的元素,事件监听回调函数中的 this 指向的是 e.currentTarget

自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
button.addEventListener('click',()=>{
const event = new CustomEvent('frank',{
detail:{
name:'frank',
age:18
},
bubbles:true
})
button.dispatchEvent(event)
})

button.addEventListener('frank',(e)=>{
console.log('frank 事件触发了')
console.log(e.detail)
})

事件委托

应用场景

  • 监听100个按钮的点击事件,监听这100个按钮的祖先,等冒泡的时候判断 target 是不是这100个按钮中的一个
  • 监听目前还不存在的元素的点击事件,监听祖先,等点击的时候看是不是想要监听的元素

优点

  • 省监听数(内存)
  • 可以监听动态元素

手写事件委托

简写版:

1
2
3
4
5
6
7
8
9
10
11
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,e=>{
const t = t.target
if(t.matches(selector)){
fn(e)
}
})
}

简写版存在一个问题,如果点击的是 selector 内部的元素,事件委托就会失效。因此需要递归判断 target 的父元素/爷爷元素等是否是 selector。完整版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function on(eventType,element,selector,fn){
if(!(element instanceof Element)){
element = document.querySelector(element)
}
element.addEventListener(eventType,e=>{
let t = t.target
while(!t.matches(selector)){
if(element===t){
t = null
break;
}
t = t.parentNode
}
t && fn.call(t,e,t)
})
return element
}

JS 和 DOM 事件的关系

DOM 事件并不是 JS 的内容,事实上 DOM 只是浏览器提供的 web api。JS 只是调用了 DOM 提供的 addEventListener 而已。而 NodeJS 实现了类似的事件系统 EventEmitter,不过没有 DOM 事件中冒泡,捕获等行为。

默认动作

许多事件会自动触发浏览器执行某些行为。比如点击表单的提交按钮会触发提交表单到服务器的行为,点击链接触发导航到该 URL 的行为。

阻止浏览器默认动作

  • 使用 event 对象的 preventDefault() 方法
  • 如果处理程序是使用 on<event> (而不是 addEventListener) 分配的,那么返回 false 也是有效的
1
2
3
<a href="/" onclick="return false">Click here</a>
or
<a href="/" onclick="(event)=>event.preventDefault()">here</a>

form 表单的正确写法

之前遇到过一个问题,在 form 表单中点击按钮自动刷新了整个页面。原因是在 chrome 浏览器中,即使 button type 没有设置为 submit ,也会被视为提交按钮,点击后会触发表单的提交动作从而导致整个页面刷新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 <template>
<form @submit.prevent="submit">
<label>
<span>用户名</span>
<input type="text" v-model="user.username" />
</label>

<label>
<span>密码</span>
<input type="password" v-model="user.password" />
</label>
<button>登录</button>
</form>
</div>
</template>

<script>
export default {
data() {
return {
user: {
username: "",
password: "",
},
};
},
methods: {
submit() {
console.log(this.user);
},
},
};
</script>

通常我们不需要 form 的默认动作,可以监听 form 的 submit 事件阻止默认动作并调用我们自定义的 submit 函数。在 form 标签中的 input 元素中回车也可以触发submit 事件,点击 label 元素会将焦点放在内部的 input 元素上。

如何阻止滚动?

  • 阻止 scroll 事件的默认动作是没用的,因为现有滚动才会有滚动事件
  • 要阻止滚动可以阻止 wheel 和 touchstart(移动端) 的默认动作
  • CSS 隐藏滚动条,避免用户拖动滚动条进行滚动

参考:

  1. 浏览器默认行为
  2. web API 参考