总结事件代理

事件处理函数

  • 绑定事件:绑定事件的处理函数
    • 事件是原本就存在的,不需要绑定,其实是在绑定「事件触发后」要处理的函数
  • 事件 + 事件的反馈 = 前端交互

事件句柄

所谓的事件句柄就是一个将元素的特定事件与某个函数关联起来,比如 onclickonmouseover 等都是句柄,它们会指向一个给定的函数,也就是事件发生时要执行的操作。例如:

1
2
3
4
5
6
div.onclick = function() {
// todo...
}

// 👆🏻 整体叫做 事件句柄,其中 onclick 单独拆出来叫 句柄
// 通常说的 事件句柄的绑定形式 就是上面这种形式

句柄

上面的 onclick 就是句柄

事件源

事件作用在谁身上,谁就是事件源。

比如网页元素中 a 标签有 onclick 事件,当点击 a 发生 onclick 事件时,事件源就是 a 标签。

🌈如何绑定事件处理函数

1. HTML中直接绑定 - 内联/行内事件监听器

1
<div onclick="test()" onmouseover="test2()"></div>

2. JS 中获取 DOM 元素后绑定

1
div.onclick = function() {};

同时绑定多个处理函数,后边的会覆盖前面的

1
2
3
4
5
6
7
8
9
10
div.onclick = function() { console.log(1); };
div.onclick = function() { console.log(2); };

// 只会打印 2
<div onclick="console.log(1)">点击我</div>
<script>
div.onclick = function() { console.log(2); };

// 只会打印 2
</script>

3. 事件监听器 addEventListener(事件类型,事件处理函数, 是否捕获)

  • IE9以下不兼容,W3C规范
  • 同一元素可以绑定多个事件,彼此不会覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
oBtn.addEventListener('click', function() {
this.innerHTML = '加载中..';
}, false)
oBtn.addEventListener('click', function() {
console.log('加载更多事件');
}, false)
// 2个绑定都会执行
oBtn.addEventListener('click', test, false);
oBtn.addEventListener('click', test, false);
function test() {
console.log(1);
}
// 只会执行一次打印,因为绑定的是同一个函数引用

4. attachEvent(事件类型,事件处理函数)

  • IE8及以下
  • this 指向 window
  • 与 addEventListener 不同,绑定同一引用值的函数,绑定几次执行几次
1
2
3
4
5
6
7
8
9
10
oBtn.attachEvent('onclick', function() {
test.call(oBtn);
})
function test() {} // 解决attachEvent 中this指向问题
oBtn.attachEvent('onclick', test, false);
oBtn.attachEvent('onclick', test, false);
function test() {
console.log(1);
}
// 执行两次打印

🌈封装事件处理函数

1
2
3
4
5
6
7
8
9
10
11
function addEvent(el, type, fn) {
if (el.addEventListener) {
el.addEventListener(type, fn, false);
} else if (el.attachEvent) {
el.attachEvent('on' + type, function() {
fn.call(el);
});
} else {
el['on' + type] = fn;
}
}

🌈解除事件绑定

  • elem.onclick = null; / elem.onclick = false;
  • elem.removeEventListener(参数需要和绑定时候的参数完全相同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
oBtn.addEventListener('click', test, false);
oBtn.removeEventListener('click', test, false);

// 函数提出来
function test() {
this.className = '';
this.innerHTML = '';
this.removeEventListenner('click', test, false);
}
// 给匿名函数起名字
oBtn.addEventListener('click', function test() {
this.className = '';
this.innerHTML = '';
this.removeEventListenner('click', test, false);
})
// 非严格模式下
oBtn.addEventListener('click', function() {
this.className = '';
this.innerHTML = '';
this.removeEventListenner('click', arguments.callee, false);
})
  • elem.detachEvent(type, fn)
1
oBtn.detachEvent('onclick', fn);

事件的冒泡和捕获

🌈冒泡和捕获

子级会向父级传递自己的事件,如果父级也有相同事件则响应

  • 冒泡:事件从 DOM 子元素向父级传递,触发父级的相同类型的事件
    • 即使子元素在视觉上不在父元素容器内,也会向上传递
    • 父元素不绑定同类型事件处理函数,祖父绑定,祖父的事件处理函数仍然会触发
  • 捕获:自嵌套关系的最顶层,向子元素传递,到事件源
  • elem.addEventListener(事件类型,事件处理函数,false) false事件冒泡,true事件捕获
  • 先捕获,后冒泡。到事件源的时候,按照绑定顺序执行
    • 新版chrome事件源不再根据绑定顺序执行,而是统一变成先捕获后冒泡,参考https://juejin.cn/post/6965682915141386254
    • 到事件源的时候,处于target阶段,没有捕获or冒泡的说法。所以不用care执行顺序。代码层面上注意下就行,最好先写捕获再写冒泡
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
<style>
.wrapper {
width: 150px;
height: 150px;
background-color: aqua;
}
.outer {
width: 100px;
height: 100px;
margin-left: 150px;
background-color: bisque;
}
.inner {
width: 75px;
height: 75px;
margin-left: 100px;
background-color: blueviolet;
}
</style>

<div class="wrapper">
<div class="outer">
<div class="inner"></div>
</div>
</div>

<script>
var wrapper = document.getElementsByClassName('wrapper')[0];
var outer = document.getElementsByClassName('outer')[0];
var inner = document.getElementsByClassName('inner')[0];

wrapper.addEventListener('click', function() {
console.log('wrapper-冒泡');
}, false);
outer.addEventListener('click', function() {
console.log('outer-冒泡');
}, false);
inner.addEventListener('click', function() {
console.log('inner-冒泡');
}, false);

wrapper.addEventListener('click', function() {
console.log('wrapper-捕获');
}, true);
outer.addEventListener('click', function() {
console.log('outer-捕获');
}, true);
inner.addEventListener('click', function() {
console.log('inner-捕获');
}, true);
</script>

新版chrome:

旧版chrome和新旧版Firefox、Safari都还是顺序执行:

  • focus blur change submit reset select 没有冒泡
  • 老版本IE没有捕获

🌈阻止事件冒泡

w3c: e.stopPropagation(); -> Event.prototype

1
2
3
4
oDiv.addEventListener('click', function(e) {
var e = e || window.event;
e.stopPropagation();
}, false);

IE: e.cancelBubble = true;

1
2
3
4
oDiv.addEventListener('click', function(e) {
var e = e || window.event;
e.cancelBubble = true;
}, false)

封装

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

案例

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
<!DOCTYPE html>
<html lang="en">
<head>
<style>
.wrapper {
position: relative;
width: 300px;
height: 300px;
background-color: green;
}

.apply {
position: absolute;
bottom: 15px;
right: 15px;
width: 80px;
height: 30px;
background-color: red;
color: #fff;
line-height: 30px;
text-align: center;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="apply">立即申请</div>
</div>

<script type="text/javascript">
// w3c: e.stopPropagation(); -> Event.prototype
// IE: e.cancelBubble = true;
var wrapper = document.getElementsByClassName("wrapper")[0],
apply = document.getElementsByClassName("apply")[0];

wrapper.addEventListener("click", function() {
console.log("进入详情");
}, false);
apply.addEventListener("click", function(e) {
console.log("立即申请");
cancelBubble(e);
}, false);

// 取消冒泡
function cancelBubble(e) {
var e = e || window.event;
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
</script>
</body>
</html>

🌈取消默认事件

取消右键菜单

return false (不支持 addEventListener 中使用)

1
2
3
4
document.oncontextmenu = function(e) {
var e = e || window.event;
return false;
}

e.preventDefault() (IE9不兼容):

1
2
3
4
document.oncontextmenu = function(e) {
var e = e || window.event;
e.preventDefault();
}

e.returnValue = false; (IE9以下):

1
2
3
4
document.oncontextmenu = function(e) {
var e = e || window.event;
e.returnValue = false
}

阻止 a 标签跳转

  • href="javascript:;"
  • href="javascript: void(0)" ==> void(0) 等于 return 0
  • e.preventDefault()
1
2
3
4
<a href="">hh</a>
a.onclick = function(e) {
e.preventDefault();
}

事件流

  • 描述从页面中接收事件的顺序, 冒泡, 捕获
  • IE 提出的 事件冒泡流 (Event Bubbling)
  • Netscape 提出的 事件捕获流 (Event Capturing)

事件流有三个阶段

  • 事件捕获阶段
  • 处于目标阶段 - 代码按先后顺序执行(新版chrome先捕获后冒泡
  • 事件冒泡阶段

🌈DOM分级

  • DOM 0

    • onclick = ""
    • ele.onclick = function() {}
    • onmouseover
    • onXXX
  • DOM 1

    • 没有定义事件模型
  • DOM 2

    • addEventListener(3个参数) -> W3C规范
    • removeEventListener

三个级别没有重要之分,可以看成不同时期定义的新的使用方案

🌈事件对象

不同浏览器区别

target、srcElement 事件源对象

  • FF火狐只有 target
  • IE 只有 srcElement
  • Chrome 两者兼有

兼容性

1
2
3
4
wrapper.onclick = function(e) {
var target = e.target || e.srcElement;
console.log(e);
}

🌈事件代理/委托

案例 - 动态添加li元素添加事件

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件委托/代理</title>
<style>
ul {
margin: 0;
padding: 20px;
list-style: none;
background-color: pink;
}
</style>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<button onclick="addLiEle()">添加 li 元素</button>
<script>
var oUl = document.getElementsByTagName('ul')[0],
oLis = document.getElementsByTagName('li');
oUl.addEventListener('click', function(e) {
var event = e || window.event,
target = event.target || event.srcElement;
if (target.tagName.toLowerCase() === 'li') {
var index = Array.prototype.indexOf.call(oLis, target);
console.log(oLis[index]);
}
}, false);

function addLiEle() {
var oLi = document.createElement('li');
var oUl = document.getElementsByTagName('ul')[0];
oLi.innerText = ++oUl.childElementCount;
oUl.appendChild(oLi);
}
</script>
</body>
</html>

事件

  • 事件触发时那一刻,所有相关信息和方法的集合
    • chrome:最外层多了个 pointerEvent
    • 其他浏览器:最外层是 mouseEvent
1
2
3
4
5
6
7
8
9
10
var container = document.querySelector('.container');
container.addEventListener('click', function(e) {
console.log(e);
});

// [pointerEvent]PointerEvent ->
// MouseEvent.prototype ->
// UIEvent.prototype ->
// Event.prototype ->
// Object.prototype

🌈event.target 和 event.currentTarget 的区别

  • target 当前触发事件的元素
  • currentTarget 绑定事件处理函数的元素