事件

参考资料:https://www.bitovi.com/blog/a-crash-course-in-how-dom-events-work

事件

事件监听

1.怎样监听一个事件
DOM0
1
2
3
<button onclick="alert('hello');">
click me!
</button>

这种直接将事件混合在HTML中,非常的不灵活

DOM1
1
2
3
4
var button = document.getElementById('button');
button.onclick = function(){
alert('click me!')
}

这种方式相对于前一种,分离了HTML和JavaScript,但是只能为一个元素绑定事件

DOM2
1
2
3
4
var button = document.getElementById('button');
button.addEventListener('click',function(){
alert('click me!')
},false)

通过addEventListener可以让我们对事件写更多的处理程序,下面会说addEventListener的第三个参数(可以控制事件在冒泡阶段还是捕获阶段触发)。

2.事件在文档中怎么穿梭
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
<!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>
</head>

<body>
<div id="myDiv">
<a id="myAnchor" href="https://qinhanwen.github.io/">qinhanwen!
</a>
</div>
</body>
<script>
var html = document.documentElement;
var body = document.body;
var div = document.getElementById('myDiv');
var a = document.getElementById('myAnchor');

function handles(e) {
console.log(e.currentTarget);
}
html.addEventListener('click', handles, true);
body.addEventListener('click', handles, true);
div.addEventListener('click', handles, true);
a.addEventListener('click', handles, true);
</script>

</html>

我们为根元素,body,div,a添加监听事件,并且addEventListener第三个参数值为false(默认为false),handler在冒泡阶段调用。

点击a之后看到打印出a => div => body => html

WX20190105-000222@2x

修改一下addEventListener第三个参数值为true,handler在捕获阶段执行

WX20190105-000914@2x

点击a之后看到打印出html => body => div => a

3.浏览器做了什么(用代码来表现事件在浏览器中的行为)
1)element.addEventListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HTMLNode.prototype.addEventListener = 
function(eventName, handler, phase){
// Make a __handlers object on
// this element if there is none already
if(!this.__handlers){
this.__handlers = {};
}

// If there are no event handler lists for
// this event, add them
if(!this.__handlers[eventName]){
this.__handlers[eventName] =
{capture : [], bubble: []};
}

// Add the new handler function
// to the specified phase list
this.__handlers[eventName]
[phase ? 'capture' : 'bubble'].push(handler);
}

addEventListener接受3个参数,事件名,回调,阶段,如果this.handlers不存在,就创建一个对象。如果这个对象没有对应事件的value,设置这个对应事件的value为{capture : [], bubble: []},然后判断phase,将handler压进对应的数组。

2)事件是怎样被调用的
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
Handle = function(ev){

// 第一步

var elements = [],//声明一个elements数组
target = ev.target,//ev.target就是触发的目标元素
isPropagationStopped = false,//设置isPropagationStopped是否阻止行为为false
isDefaultPrevented = false;//isDefaultPrevented是否阻止默认行为为false

ev.eventPhase = 1;

ev.stopPropagation = function(){//设置阻止行为的方法,去修改isPropagationStopped的值为true(这个事件会阻止冒泡或捕获,下面举例子)
isPropagationStopped = true;
}

ev.preventDefault = function(){//设置阻止默认事件的方法,去修改isDefaultPrevented的值为true
isDefaultPrevented = true;
}

//第二步

do{
elements.push(target);//向elements里压进target
}while((target = target.parentNode)); //把target的父节点赋值给target,直到target父节点为空的时候,循环终止

elements.reverse();//获取的elements数组,排序是从里到外,而捕获是从外到里,所以数组反转一下


//第三步

//遍历elements数组
for(var i = 0 ; i < elements.length; i++){


if(isPropagationStopped){//如果阻止行为,就跳出循环
break;
}

var currentElement = elements[i],//currentElement变量保存遍历的当前元素

//如果这个元素有绑定handler,type和phase,把数组赋值给handlers,否则设置它为一个空数组
handlers = currentElement.__handlers
&& currentElement.__handlers[ev.type]
&& currentElement.__handlers[ev.type].capture
|| [];

ev.currentTarget = currentElement;//设置currentTarget为当前的这个元素

//循环调用handlers里的回调(如果有多个监听事件的话,handlers数组的元素会有多个)
for(var h = 0; i < handlers.length; h++){
handlers[h].call(currentElement, ev);
}
}

//第四步
if(!isPropagationStopped){
ev.target["on" + ev.type].call(ev.target, ev);//触发DOM1事件
}

elements.reverse();//冒泡阶段从里到外,所以再次反转数组
ev.eventPhase = 3;


//第五步


for(var i = 0 ; i < elements.length; i++){//遍历elements
if(isPropagationStopped){
break;
}

//跟上面的捕获相反,这里是冒泡
var currentElement = elements[i],
handlers = currentElement.__handlers
&& currentElement.__handlers[ev.type]
&& currentElement.__handlers[ev.type].bubble
|| [];

ev.currentTarget = currentElement;

//循环调用handlers里的回调(如果有多个监听事件的话,handlers数组的元素会有多个)
for(var h = 0 ; i < handlers.length; h++){
handlers[h].call(currentElement,ev);
}
}

//第六步

// 元素的默认行为
if(!isDefaultPrevented){//如果没有阻止默认行为(这里是a元素的)

if(ev.type == "click"
&& ev.target.nodeName.toLowerCase() == "a"){
window.location = ev.target.href;
}

}
}

通过上面代码伪实现可以看到stopPropagation在捕获和冒泡阶段都有起作用。

总结

1.element.addEventListener方法,会判断第三个参数phase(默认是false),如果是true就声明(或者向已声明的)capture数组里push进函数。如果是false就向bubble数组内push进函数
2.假设监听的是click事件,在点击目标元素的时候,计算路径,由内到外的获取节点并且push到elements数组里,反转elements数组(变成从外到内),然后遍历elements数组(如果有调用stopPropagation就跳出循环),遍历elements中每个节点的capture数组,并且调用。
3.调用被点击元素的DOM1事件,再次反转elements数组,变成从内到外。
4.遍历elements数组的,遍历每个节点的bubble数组,并且调用。
5.如果没有调用preventDefault方法,就是调用标签的默认行为(比如a标签的跳转)。

总结之后发现漏了 事件代理,赶紧补一下

其实说的比较简单就是:有100个li元素,我点击了某个li元素,于是冒泡到了ul,触发了ul的监听事件,然后在这里再对点击事件做处理。

直接写🌰吧

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
<!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>
</head>

<body>
<ul id="ul">

</ul>
</body>
<script>
var arr = [1, 2, 3, 4, 5, 6];
var str = '';
var ul = document.getElementById('ul');
ul.addEventListener('click', function (e) {
console.log(e.target.dataset.id);
})
arr.forEach(function (i) {
str += '<li data-id="' + i + '">' + i + '</li>'
})
ul.innerHTML = str;
</script>

</html>

其实我们做的事情就是为li添加了自定义属性,然后点击的时候冒泡,打印出被点击的元素的自定义属性。

我们分别点击1到6,看到控制台打印出了自定义的id

WX20190111-222400@2x

嗯,大概就这样子,那么它的有优点是:

​ 首先,每个函数都是对象,我如果为每个li都添加的话,如果数量很多的话,会占用很多内存,影响性能。然后,网上很多人说处理程序越多,需要不断的对DOM操作,会导致浏览器不断的回流和重绘,其实这点我觉得像之前的这种,点击某个li,大概就是改个颜色或者改一下li的内容,应该和冒泡到ul之后再处理是差不多的,这点还没能很好的理解,之后理解了再来改。

缺点:它是利用冒泡,如果有一些元素没有冒泡,比如input。