JavaScript事件流

一、前言

javascript中的事件流清楚吗,可以简单描述一下过程吗

事件委托是什么,原理又是什么?

果然一面试脑子就发懵,事件流很久之前看过,后来忘了

包括vue中有些指令也有.stop等修饰符,其原理也都是js事件流的执行过程

二、JavaScript中的事件流

首先了解一下一个基本概念,也是很容易忽视的一个点,面试官可能会从这个问题引出事件流,面试官:“JS有哪些事件类型”

答案是: 鼠标事件, 键盘事件, HTML事件

2.1 JS基本事件类型

这部分我自己排着捋了一遍,复习的话建议看一看,如果只对事件流感兴趣,可以直接跳转2.2.2

鼠标事件主要包括:

  1. click
  2. dblckick
  3. mousedown 鼠标按下还未弹起时触发
  4. mouseup 鼠标释放时触发
  5. mouseover 移动到元素上方时触发
  6. mouseout 鼠标移开时触发
  7. mousemove 在元素上移动时触发
  8. mouseenter 鼠标进入
  9. mouseleave 鼠标离开

注意: mouseover和mouseenter的区别是,mouseover会触发事件冒泡,容易记混,记得mouseover里面有个o,把它当成那个 把。。。

还可以通过打印事件对象event,查看bubbles属性,经过验证mouseenter打印的事件对象bubbles属性为false,也就是不冒泡,而mouseover则为true

键盘事件主要包括:

  1. keydown
  2. keyup
  3. keypress

注意: keydown和keypress的区别是,keydown只能触发字符,比如abcd1234,不能触发ctrl这种按键的事件,

keypress基本都可以触发

HTML事件主要包括:

  1. load 页面加载完window上触发的回调
  2. unload load相反
  3. select input或者textarea选择文本
  4. change 输入框内容改变且失去焦点时触发
  5. input 输入时触发
  6. focus 元素获得焦点时触发
  7. blur 元素失去焦点时触发
  8. submit 提交表单时触发,但是无法阻止提交表单
  9. reset 重置表单时触发
  10. resize 窗口大小改变在window上触发的事件
  11. scroll 当用户滚动带滚动条的元素时触发

2.2 事件流的概念

事件流描述的就是从页面中接收事件的顺序

2.2.1 DOM发展历程

W3C协会早在1988年就开始了DOM标准的制定,W3C DOM标准可以分为DOM1,DOM2,DOM3三个版本。

1. DOM 0级

DOM0级事件具有极好的跨浏览器优势,会以最快的速度绑定。首先记住结论:DOM 0级中只支持事件冒泡

第一种方式是内联模型(行内绑定),将函数名直接作为html标签中属性的属性值。

1
2
3
4
5
6
<div onclick="btnClick()">click</div>
<script>
function btnClick(){
console.log("hello");
}
</script>

第二种方式是脚本模型(动态绑定),通过在JS中选中某个节点,然后给节点添加onclick属性

记住这里也有个面试题,两次注册onclick,点击事件触发的时候会执行几次?

答案是一次,因为是修改的节点的属性,第二次会覆盖第一次, 就像给id赋值两次一样,最终只有一个id

1
2
3
4
5
6
7
<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
console.log("hello");
}
</script>

1级DOM标准中并没有定义事件相关的内容,所以没有所谓的1级DOM事件模型

2. DOM 2级

在DOM2级中就定义了2.2.2中将要提及的事件流的三部分捕获阶段, 目标阶段, 冒泡阶段, 同时定义了两个绑定事件的方法,分别是

1
2
addEventListener(type, listener, useCapture)
removeEventListener(type, listener[, options])

addEventListener第三个是useCapture,也就是是否启用事件捕获,也就是在捕获阶段触发,其默认值为false,也就是默认是使用冒泡机制

个人理解是:事件传播只有第二三阶段,即目标阶段冒泡阶段。其实这个理解是错误的,事件传播还是这个流程,只是在捕获阶段不会触发对应的listener,也不会执行绑定的函数而已

3. DOM3级

在这个版本更新中完善了一些事件,好像没其他什么需要说明的

2.2.2 事件流三个过程

在早期,IE提出事件冒泡流,NetScape提出事件捕获流,为了协调二者,ECMAScript在DOM2中提出事件流包含三个阶段,分别是捕获阶段,处于目标阶段,冒泡阶段,三者顺序为捕获 -> 目标阶段 -> 冒泡

捕获阶段(capturing)

事件从Document对象沿着文档树向下传播给节点。如果目标的任何一个祖先专门注册了事件监听函数,那么在事件传播的过程中就会运行这些函数

目标执行阶段

到达触发事件的源,会执行注册的回调函数(事件监听函数)

冒泡阶段(bubbling)

这个阶段事件将从目标元素向上传播回Document对象(与capturing相反的阶段)。虽然所有事件都受capturing阶段的支配,但并不是所有类型的事件都bubbling(上面说的mouseenter就没有事件冒泡,mouseover有冒泡)

2.2.3 事件对象Event

在触发DOM上的某个事件时,会产生一个事件对象event,这个事件对象在DOM 0级中需要通过window对象获取,在DOM2级及以后回直接将事件对象添加到回调函数的第一个参数中

1
2
3
document.getElementById("app").onclick = function(e){
e = e || window.event; // 兼容IE模型,或者说DOM 0级中的写法
}

事件对象Event主要包括以下属性:

以下内容摘自简书,原文: JS中DOM0,DOM2,DOM3级事件模型解析

  1. bubble : 表明事件是否冒泡

  2. cancelable : 表明是否可以取消冒泡

  3. currentTarget : 当前事件程序正在处理的元素, 和this一样的;

  4. defaultPrevented: false ,如果调用了preventDefualt这个就为真了;

  5. detail: 与事件有关的信息(滚动事件等等)

  6. eventPhase: 值为1表示处于捕获阶段, 值为2表示处于目标阶段,值为3表示在冒泡阶段

  7. target || srcElement: 事件的目标

  8. trusted: 为ture是浏览器生成的,为false是开发人员创建的(DOM3)

  9. type : 事件的类型 view : 与元素关联的window, 我们可能跨iframe

  10. preventDefault() 取消默认事件

  11. stopPropagation() 取消冒泡或者捕获

  12. stopImmediatePropagation() (DOM3)阻止任何事件的运行; stopImmediatePropagation阻止绑定在事件触发元素的 其他同类事件的callback的运行 IE下的事件对象是在window下的,而标准应该作为一个参数, 传为函数第一个参数; IE的事件对象定义的属性跟标准的不同,如: cancelBubble 默认为false, 如果为true就是取消事件冒泡; returnValue 默认是true,如果为false就取消默认事件; srcElement, 这个指的是target, Firefox下的也是srcElement

三、事件委托(事件代理)

事件委托函数是指把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。

事件委托基于事件冒泡机制,点击子元素,通过事件冒泡将事件冒泡到父元素,所以只需要在父元素添加对应的事件处理函数即可

如下:

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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>事件代理</title>
</head>
<body>
<ul id="list">
<li class="item"><a class="link" href="http://www.baidu.com">百度</a></li>
<li class="item"><a class="link" href="https://www.163.com/">网易</a></li>
<li class="item"><a class="link" href="https://www.qq.com/">腾讯</a></li>
</ul>
<script>
let ul = document.getElementById("list");
ul.addEventListener("click", function(e) {
if (e.target.className == "item") {
console.log(e.target.childNodes[0].href);
}
if (e.target.className == "link") {
e.preventDefault();
console.log("取消默认行为")
}
})
</script>
</body>
</html>

给ul绑定的事件,最终实现效果是点击li和a也会触发行为,这就是事件代理,也就是事件冒泡的好处(最开始总觉得冒泡就是给我制造麻烦。。还要取消冒泡,现在发现,哎,还挺有趣)

注意上面的例子,target不是绑定给谁就是谁,而是谁触发,target就是谁,如果是li触发,那target就是li

3.1 好处

  1. 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
  2. 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以一样具有和其他元素一样的事件。

第一点没啥好说的,第二点举个例子

如果是把事件直接绑定到li上,那么如果动态操作dom,新增加的li就不会有对应的事件(因为没有注册事件),而通过事件代理,就会解决这个问题

上面事件代理的例子,增加一个button,绑定事件,点击的时候动态给ul增加一个li

1
2
3
4
5
6
7
8
9
10
11
<button id="btn">添加dom节点</button>
<script>
// id为btn,可以直接通过btn访问
btn.onclick = function() {
let newOne = document.createElement("li");
newOne.className = "item";
newOne.innerHTML = `<a class="link" href="https://www.test.com/">测试</a>`;
ul.appendChild(newOne);
}
</script>

这样即使是新增加的li节点也会有对应的点击事件,因为只要点击就会有冒泡,冒泡到父级就会触发相应的函数

四、例子

几个例子验证事件流

4.1 DOM 0级中的事件冒泡机制

1
2
3
4
5
6
7
8
9
10
11
12
13
<ul onclick="ulClick()" style="padding:15px">
<div>
<li onclick="liClick()">li</li>
</div>
</ul>
<script>
function ulClick() {
console.log("ul");
}
function liClick() {
console.log("li");
}
</script>

点击li会打印 liul, 点击ul只会打印 ul

事件从li向上冒泡的过程中,如果节点有对应的执行函数,则会执行,没有则继续向上冒泡,如本例中的div,如果div也绑定点击事件,则也会触发,触发顺序在li和ul中间

4.2 DOM2级中的事件流的过程

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
<ul id="ul" style="padding:15px;background-color: #7FFFD4;">
<div id="div" style="padding:15px;background-color: #ccc;">
<li id="li">li</li>
</div>
</ul>
<script>
ul.addEventListener("click", function() {
console.log("ul捕获时触发");
}, true);
ul.addEventListener("click", function() {
console.log("ul冒泡时触发")
}, false);
div.addEventListener("click", function() {
console.log("div捕获时触发");
}, true);
div.addEventListener("click", function() {
console.log("div冒泡时触发")
}, false);
li.addEventListener("click", function() {
console.log("li捕获时触发");
}, true);
li.addEventListener("click", function() {
console.log("li冒泡时触发")
}, false);
</script>

点击li后触发顺序依次为 ul捕获时触发div捕获时触发li捕获时触发li冒泡时触发div冒泡时触发ul冒泡时触发和DOM 2级中说的一样,如下图

image20210525111831786.png

如果在li的点击事件中取消冒泡,则点击li只会打印三个捕获,而不会冒泡

4.3 关于stopPropagation的误解

还是上面的例子,如果在div的捕获事件中调用事件对象的stopPropagation方法,思考点击li会打印哪些东西

1
2
3
4
div.addEventListener("click", function(e) {
e.stopPropagation();
console.log("div捕获时触发");
}, true);

正确的答案是打印 ul捕获时触发 -> div捕获时触发 然后结束

最初以为stopPropagation只是阻止事件冒泡,其实不是,mdn中对这个方法这样描述的

阻止捕获和冒泡阶段中当前事件的进一步传播。

也就是碰到这个方法,事件流就会提前结束,捕获和冒泡都会停止