阿瑞斯的BLOG

JavaScript 事件

关键词
事件流 事件冒泡 事件捕获 事件委托 事件处理函数被执行的时机 自定义事件 事件触发的三个阶段 事件对象 事件目标元素

什么是事件流?什么是IE事件流?IE事件流和DOM事件流的区别是什么?

事件流描述的是从页面中接收到事件的顺序,IE的事件流是事件冒泡(event bubbling),而Netscape提出和支持的是事件捕获流,但由于Netscape已经被淘汰了,所以没有只支持事件捕获的浏览器了,而DOM 2级事件模型是把两种事件合并起来了。

DOM事件流的目的旨在通过事件捕获为截获事件提供了机会,通过事件冒泡设置处理程序响应事件。

#事件触发的三个阶段

1.document 往事件触发地点,捕获前进,遇到相同注册事件立即触发执行

2.到达事件位置,触发事件

3.事件触发地点往 document 方向,冒泡前进,遇到相同注册事件立即触发

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
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#parent {
width: 200px;
height: 200px;
background-color: blue
}
#child {
background-color: red;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="parent">
<div id="child"></div>
</div>
<script>
document.getElementById('parent').onclick = function() {
alert('father emit!');
}
document.getElementById('child').onclick = function() {
alert('child emit!');
}
</script>
</body>
</html>

从上个例子的结果child emit!father emit!可以看出,通过onload注册的事件仅仅支持冒泡,也就是说通过DOM 0级的注册的事件处理程序仅仅只是在冒泡阶段被处理。

假如一个元素既注册了冒泡,也注册了捕获,是捕获先被触发呢还是冒泡先被触发呢?

根据DOM2级事件流,捕获——到达——冒泡的顺序,应该是先发生捕获,再发生冒泡。

测试结果:

  1. 点击父元素,父元素捕获——父元素冒泡,符合先捕获后冒泡的事件流机制。

  2. 点击子元素,父元素捕获——子元素冒泡——子元素捕获——父元素冒泡,这里可以发现一个很重要的点,即当一个元素同时存在捕获和冒泡时,会按照注册的时候响应事件处理程序

  3. 如果是通过DOM 0级注册的冒泡,通过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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#parent {
width: 200px;
height: 200px;
background-color: blue
}
#child {
background-color: red;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="parent">
<div id="child"></div>
</div>
<script>
// 父元素按照先捕获后冒泡的方式注册
document.getElementById('parent').addEventListener('click',function() {
alert('parent event capturing emit!');
},true);
document.getElementById('parent').addEventListener('click',function() {
alert('parent event bubbling emit!');
},false);
// 子元素按照先冒泡后捕获的方式注册
document.getElementById('child').addEventListener('click',function() {
alert('child event bubbling emit!');
},false);
// document.getElementById('child').onclick = function() {
// alert('child event bubbling emit!');
// } [3]
document.getElementById('child').addEventListener('click',function() {
alert('child event capturing emit!');
},true);
</script>
</body>

对事件流和事件处理程序的理解

个人认为事件流在DOM2级以后,就是一种事件发生时,事件在DOM中逐级传播的正常现象,这种现象不由我们控制,而我们能做的只是决定我们需要在什么阶段响应事件。是冒泡阶段or捕获阶段? 这个由我们来决定。

事件处理程序则是响应事件发生做点什么,事件处理程序被调用依赖于两个条件:事件传播到了注册的元素上且在指定的阶段。

举例,假如一个事件同时存在事件捕获和事件冒泡,事件捕获先注册,事件冒泡后注册,那么如果在事件捕获时通过调用e.stopPropagation()阻止事件的进一步传播,那是不是意味着冒泡阶段的事件处理程序不会再被调用?

注意,这里e.stopPropagation()不会取消自身注册的冒泡阶段的事件处理程序响应!!!会触发;而想要在自身同时存在冒泡和捕获的时候取消第二次事件处理程序响应,需要通过e.stopImmediatePropagation()阻止任何事件处理程序被调用。(这也是这2个API的区别)

为什么我们通常在冒泡阶段调用处理程序?

  1. 因为在跨浏览器的事件处理程序中,因为IE只支持冒泡,为了保证事件处理代码在大多数浏览器下一致性的运行的目的。所以事件处理程序被调用的时机也就只关注 冒泡阶段了。

  2. 事件冒泡允许多个操作被集中处理(把事件处理器添加到一个父级元素上,避免把事件处理器添加到多个子级元素上),它还可以让你在对象层的不同级别捕获事件。这里可能存在疑问:我们把事件处理程序挂在父级元素上,如何在处理程序中去操作子元素? 一般挂在子元素中,我们明确知道this指向的就是子元素,即操作我们的目标元素,那么在父元素中如何操作子元素呢?
    不得不提的e.target 属性,这个属性会返回事件触发的目标元素,注意这个目标元素一定是唯一的!举例来说:document,parent,child,点击了parent区域就一定是parent,点击了child区域就一定是child,而不会像事件流那样的既是child,也是parent!!!

注意:这里再次说明了事件流不是毫无目的 的“随意流”,而是流到事件的目标元素。

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
34
35
36
e.target 类型 Element
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#parent {
width: 200px;
height: 200px;
background-color: blue
}
#child {
background-color: red;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="parent">
<div id="child"></div>
</div>
<script>
// 把事件处理挂在父元素上,
parent.addEventListener('click',function(e) {
alert(e.target.id);
let _this = e.target; // 获取到目标元素,也就是事件实际的目标
},false);
</script>
</body>
</html>
  1. 让不同的对象同时捕获同一事件,并调用自己的专属处理程序做自己的事情,就像老板一下命令,各自员工做自己岗位上的工作去了。之前分别给parent,child注册的click事件,就是同一事件各自响应各自的处理程序。

事件冒泡的意义以及应用

事件委托:只指定一个事件处理程序,就可以管理某一类型的所有事件。

原理:利用事件冒泡机制,给父元素绑定事件处理程序并指定事件处理程序的响应阶段为冒泡阶段,这个时候点击子元素,由于子元素没有事件处理程序,所有没有任何反应,但是事件流会继续向上冒泡,也就是冒泡到父元素,父元素有处理程序,并且父元素可以知道实际上事件发生时的目标元素——通过e.target => 这个是核心

核心: 通过e.target可以确定实际触发的目标元素

好处:前者少了一个遍历所有li节点的操作,所以在性能上肯定是更优的;其次当我们绑定完事件以后又动态添加了一些元素,这时候如果是给每个子元素绑定事件,那么为了效果,我们还要为新添加的子元素绑定一次事件,重复操作且代码冗余。

event对象为什么这么重要呢?

因为event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样(原生事件直接封装好)。

关于currentTarget和target属性

对象this始终等于currentTarget的值,而target的值只包含事件的实际目标。(触发事件的实际目标) 如果直接将事件处理程序指定给目标元素,则this,currentTarget,target包含相同的值。差异体现在冒泡上。

自定义事件和DOM中的事件模拟

DOM中的事件模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// step1:创建event对象
var event = document.createEvent({string}) // 该方法创建一个event对象,该方法接收一个参数,表示创建的事件类型的字符串(指定好的),所以字符串在DOM2级中使用复数形式,而在DOM3级中使用了单数形式,这也是为什么有s和没s没区别的原因。
// step2:初始化事件
event.initEvent(eventName, canBubble, preventDefault) // 分别表示事件名称,是否可以冒泡,是否阻止事件的默认操作。
// step3:设置事件发生时的参数
event.target = this;
event.message = 'sr';
...
根据不同的实际需求设置
// step4: 绑定到DOM元素上,此时这个事件就跻身“官方事件”
$(dom).addEvent(eventName,function() {
alert('hahahaha')!
}, false);
// step5: 触发事件
$(dom)dispatchEvent(event) // 所有支持事件的DOM节点都支持这个方法。调用dispatchEvent()方法时,需要传入一个参数,即表示要触发事件的event对象。

所以综上:DOM中的事件模拟,包括

  1. 创建一个指定类型的事件实例
  2. 初始化事件:事件名称,是否冒泡,是否阻止事件的默认行为
  3. 设置事件对象的属性(模拟那些已有的,可以在事件处理程序中通过传参获得)
  4. 注册事件
  5. 触发事件

如何理解触发事件?

对于dispatchEvent触发的理解——任何点击、
各种交互其实都只是形式,形式内在也是封装了dispatch()罢了

为什么要模拟DOM事件?

大部分事件本身是由用户交互来触发的,但是有些时候我们想让程序自己来触发事件,这个时候就需要手动调用函数来实现触发事件,并传入参数event,作为当这个事件触发时传递出去的信息。

有什么用?

这个触发事件的机制由我们自己来决定了,我们可以设置自己的条件逻辑

eg:

1
2
3
if(...) {
Element.dispatchEvent(e)
}

然后对这个事件有监听的DOM元素就会被监听到,此时事件处理程序响应。

补充

如果是模拟一些比较复杂的DOM事件,比如鼠标事件,官方提供了很多事件对象的属性,用于在模拟的鼠标事件发生时传递一些信息:包括元素相对于屏幕,视口的坐标等…

自定义事件

  • 自定义事件的本质:观察者模式
  • 基于原生事件,触发依赖于原生事件的触发机制
  • 自定义事件没有浏览器帮你,需要自行设置触发
  • 为什么要有自定义事件?站在一个更高的层次上进行封装和对客观世界的描述,比如我们会说 点击盒子触发xx事件,而不会说点击某个div,btn这样的DOM节点
  • 优点:跳出原生事件的限制,提高了封装的抽象层级
  • 对比原生事件: