绝了,框架用多了原生基本都不记得了,今天刷到了这个题顺手写篇博客

正文

事件流的阶段

DOM事件流分为三个阶段:捕获阶段处于目标阶段冒泡阶段。DOM事件流会先调用捕获阶段的处理函数,然后调用目标阶段的处理函数,最后调用冒泡阶段的处理函数。

在这里,1-3是捕获阶段,4-5是处于目标阶段,6-8是冒泡阶段

绑定事件的方法

HTML内联绑定

1
<div id="box1" onclick="console.log('box1 内联绑定')"></div>

DOM属性绑定

1
2
3
4
let box1 = document.querySelector("#box1");
box1.onclick = function () {
console.log('box1 DOM属性绑定');
}

通过addEventListener绑定

为什么不看看万能的MDN

不过方便懒得跳转的同学,我贴心的截了张图(其实主要是这个函数太常用了感觉没必要说明用法)

file

tips

addEventListener可以多次给一个元素绑定多个监听同一个事件的回调,而DOM属性绑定和HTML内联绑定都只能绑定一个事件,之后绑定的事件会覆盖之前绑定的事件,而且两者的事件也会相互覆盖

HTML内联绑定和DOM属性绑定的事件都是绑定到冒泡阶段,addEventListener绑定的事件可以选择绑定到冒泡阶段和捕获阶段

事件的触发顺序

1
2
3
4
5
<div id="box1" onclick="console.log('box1 内联绑定')">
<div id="box2">
<div id="box3"></div>
</div>
</div>
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
let box1 = document.querySelector("#box1");
let box2 = document.querySelector("#box2");
let box3 = document.querySelector("#box3");

box1.addEventListener('click', function(){
console.log('box1 冒泡阶段 这是在onclick前绑定的');
},false);

box2.addEventListener('click', function(){
console.log('box2 冒泡阶段 这是在onclick前绑定的');
},false);

box2.onclick = function () {
console.log('box2 onclick');
}

box2.addEventListener('click', function(){
console.log('box2 冒泡阶段 这是在onclick后绑定的');
},false);


box3.addEventListener('click', function(){
console.log('box3 冒泡阶段 这是在onclick前绑定的');
},false);

box3.onclick = function () {
console.log('box3 onclick');
}

box3.addEventListener('click', function(){
console.log('box3 冒泡阶段 这是在onclick后绑定的');
},false);



box1.addEventListener('click', function(){
console.log('box1 捕获阶段1');
},true);
box1.addEventListener('click', function(){
console.log('box1 捕获阶段2');
},true);
box2.addEventListener('click', function(){
console.log('box2 捕获阶段1');
},true);
box3.addEventListener('click', function(){
console.log('box3 捕获阶段1');
},true);
box2.addEventListener('click', function(){
console.log('box2 捕获阶段2');
},true);
box3.addEventListener('click', function(){
console.log('box3 捕获阶段2');
},true);



box1.onclick = function () {
console.log('box1 onclick');
}

box1.addEventListener('click', function(){
console.log('box1 冒泡阶段 这是在onclick后绑定的');
},false);

页面是这样的
file

点击一下中间的box3
file

为什么是这样呢,我们分析一下

  • 首先,事件流处于捕获阶段,从外到内触发dom绑定的捕获事件,对同一个DOM,通过addEventListener绑定的捕获函数会按绑定的顺序触发。
  • 然后,事件进入到目标阶段,这个阶段不区分捕获和冒泡事件,所有事件按照绑定的顺序触发。
  • 最后,事件进入到冒泡阶段,这个阶段按照绑定的顺序触发绑定的冒泡事件,各个DOM的事件是从内到外触发的

处于捕获阶段时,从外到内触发各个DOM绑定的捕获事件,同一个DOM的捕获事件按绑定的先后顺序触发, box1,box2按下面的顺序绑定了回调

1
2
3
4
5
6
7
8
9
10
11
12
box1.addEventListener('click', function(){
console.log('box1 捕获阶段1');
},true);
box1.addEventListener('click', function(){
console.log('box1 捕获阶段2');
},true);
box2.addEventListener('click', function(){
console.log('box2 捕获阶段1');
},true);
box2.addEventListener('click', function(){
console.log('box2 捕获阶段2');
},true);

所以触发顺序是box1 捕获阶段1 -> box1 捕获阶段2 -> box2 捕获阶段1 -> box1 捕获阶段2

处于目标阶段时,不区分事件,全部按照绑定的顺序触发,box3按下面的顺序绑定了回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
box3.addEventListener('click', function(){
console.log('box3 冒泡阶段 这是在onclick前绑定的');
},false);
box3.onclick = function () {
console.log('box3 onclick');
}
box3.addEventListener('click', function(){
console.log('box3 冒泡阶段 这是在onclick后绑定的');
},false);
box3.addEventListener('click', function(){
console.log('box3 捕获阶段1');
},true);
box2.addEventListener('click', function(){
console.log('box2 捕获阶段2');
},true);
box3.addEventListener('click', function(){
console.log('box3 捕获阶段2');
},true);

所以触发顺序如下:
box3 冒泡阶段 这是在onclick前绑定的 -> box3 onclick -> box3 冒泡阶段 这是在onclick后绑定的 -> box3 捕获阶段1 -> box3 捕获阶段2

最后是冒泡阶段,冒泡阶段是从内到外的,所以会先触发box2的冒泡事件再触发box1的冒泡事件,对同一个DOM而言,触发顺序安装绑定的顺序触发

box2绑定的冒泡事件顺序

1
2
3
4
5
6
7
8
9
box2.addEventListener('click', function(){
console.log('box2 冒泡阶段 这是在onclick前绑定的');
},false);
box2.onclick = function () {
console.log('box2 onclick');
}
box2.addEventListener('click', function(){
console.log('box2 冒泡阶段 这是在onclick后绑定的');
},false);

box1绑定的冒泡事件顺序

1
2
3
4
5
6
7
8
9
box1.addEventListener('click', function(){
console.log('box1 冒泡阶段 这是在onclick前绑定的');
},false);
box1.onclick = function () {
console.log('box1 onclick');
}
box1.addEventListener('click', function(){
console.log('box1 冒泡阶段 这是在onclick后绑定的');
},false);

所以触发顺序是这样的

box2 冒泡阶段 这是在onclick前绑定的 -> box2 onclick -> box2 冒泡阶段 这是在onclick后绑定的 ->
box1 冒泡阶段 这是在onclick前绑定的 -> box1 onclick -> box1 冒泡阶段 这是在onclick后绑定的

其他小知识

阻止事件继续传递

使用e.stopPropagation(),在捕获和冒泡阶段使用都可,之后事件都继续在DOM上传递,但是在当前DOM上的事件依然会依次执行

略微修改上面的代码,我们修改box1的捕获事件

1
2
3
4
box1.addEventListener('click', function(event){
console.log('box1 捕获阶段1');
event.stopPropagation();
},true);

点击box3,事件不会继续向其他DOM传递了
file

如果你要阻止同一个DOM的相同事件继续被触发,可以使用e.stopImmediatePropagation()

1
2
3
4
box1.addEventListener('click', function(event){
console.log('box1 捕获阶段1');
event.stopImmediatePropagation();
},true);

file

不过喜欢搞事情的IE总是能搞特殊,它使用e.cancelBubble = true这样的方式来阻止事件冒泡(旧版IE不支持事件捕获)

所以完整代码应该是这样的

1
2
3
4
5
6
7
8
function stopPropagation(event) {
event = event || window.event;
if (event && event.stopPropagation){
event.stopPropagation();
}else{
event.cancelBubble = true;
}
}

阻止默认事件

如果你要阻止一些默认事件,比如href的链接跳转,submit的表单提交等,可以使用event.preventDefault()或者在绑定的函数里return false来搞定

1
2
3
4
5
6
7
a.onclick = function(){
return false;
};

a.onclick = function(event){
return event.preventDefault();
};

还是我们的IE喜欢搞特殊,IE使用e.returnValue = false来取消默认事件,所以完整的代码是这样的

1
2
3
4
5
6
7
8
9
function stopDefault(event) {
event = event || window.event;
if (event && event.preventDefault){
event.preventDefault();
}else{
event.returnValue = false;
}
return false;
}

后记

困了,睡了,没有后记