合成事件

React中的合成事件(SyntheticEvent)是对浏览器原生事件的封装,它提供了一个跨浏览器的事件系统,确保你在React中处理事件时拥有一致的体验。React合成事件并非真实的浏览器事件,而是React根据浏览器原生事件构建的跨浏览器的抽象。

合成事件的主要特点包括:

  1. 跨浏览器一致性:合成事件封装了原生事件,确保事件对象在不同的浏览器中具有相同的属性和行为。
  2. 性能优化:React通过事件委托来管理事件。无论有多少个事件处理器,所有的事件监听都被挂载到最外层的容器上(通常是 root),这种方式避免了大量的DOM事件监听器,这样可以提高性能。
  3. 自动清理:React会自动管理合成事件归还和重用,以减少内存占用。
  4. 插件系统:React事件可以很容易地通过插件进行扩展。

一个React合成事件的例子:

1
2
3
4
5
6
7
8
9
10
class MyComponent extends React.Component {
handleClick = (event) => {
console.log('Button clicked!');
console.log(event); // 这里的event是一个SyntheticEvent实例
}

render() {
return <button onClick={this.handleClick}>Click me</button>;
}
}

在这个例子中,当按钮被点击时,handleClick 方法会被调用,并且接收到的 event 参数是一个 SyntheticEvent 实例。这个 SyntheticEvent 实例封装了原生的浏览器事件,提供了一个统一的API接口,例如 event.stopPropagation()event.preventDefault()

需要注意的是,React的合成事件与原生事件不同,合成事件在事件回调结束后会被清理,它们的属性在事件回调外部是无法异步访问的,除非你调用 event.persist() 方法来保留事件。

事件、事件绑定、事件委托

在前端开发中,事件、事件绑定和事件委托是交互设计的重要组成部分。以下是对这三个概念的详细解释:

  1. 事件(Events):
    • 事件是在用户或浏览器窗口(Document Object Model,简称DOM)中发生的动作或行为,比如点击(click)、双击(dblclick)、鼠标移入(mouseenter)、鼠标移出(mouseleave)、按下键盘键(keydown)、松开键盘键(keyup)、文档加载(load)、窗口缩放(resize)等。
    • 事件可以是由用户行为触发的,比如鼠标点击,也可以是由浏览器自动触发的,比如网页完成加载。
  2. 事件绑定(Event Binding):
    • 事件绑定是将事件监听器(或处理函数)关联到DOM元素上的过程,以便当事件发生时,对应的函数会被执行。
    • 在JavaScript中,使用addEventListener方法可以实现事件绑定。例如,element.addEventListener('click', function)会在用户点击该元素时调用指定的函数。
    • 事件绑定确保了当特定事件发生时,开发者可以定制化的响应这些事件。
  3. 事件委托(Event Delegation):
    • 事件委托是事件处理的一种技术,它利用了事件冒泡(事件从发生的元素向上逐级传递)的原理。在事件委托中,不是直接在目标元素上绑定事件处理器,而是在其父元素上绑定事件处理器,然后根据事件的目标元素来决定是否执行某个操作。
    • 这种做法的优点是减少了事件处理器的数量,节约内存,同时当新增子元素时,无需额外绑定事件处理器,因为事件会冒泡到已经绑定事件处理器的父元素,可以在那里统一处理。
    • 使用事件委托可以极大地简化事件处理,特别是在处理动态内容时,比如用JavaScript动态添加的列表项。

事件传播机制

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
<!--
* @Description:
* @Author: xiuji
* @Date: 2024-01-11 15:40:55
* @LastEditTime: 2024-01-11 15:46:55
* @LastEditors: Do not edit
-->
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}

body {
width: 100%;
height: 100%;
}

.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

#root {
width: 200px;
height: 200px;
background-color: #ccc;
}

#outer {
width: 100px;
height: 100px;
background-color: #999;
}

#inner {
width: 50px;
height: 50px;
background-color: #666;
}
</style>
</head>

<body>
<div id="root" class="center">
<div id="outer" class="center">
<div id="inner" class="center"></div>
</div>
</div>
<script>
const root = document.getElementById('root')
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
// true 代表捕获阶段,false 代表冒泡阶段
root.addEventListener('click', () => {
console.log('root 捕获')
}, true)
root.addEventListener('click', () => {
console.log('root 冒泡')
}, false)

outer.addEventListener('click', () => {
console.log('outer 捕获')
}, true)
outer.addEventListener('click', () => {
console.log('outer 冒泡')
}, false)

inner.addEventListener('click', (ev) => {
ev.stopImmediatePropagation() // 阻止事件的进一步传播,包括阻止调用同一元素上的其他事件监听器,可以确保当前元素上绑定的其他事件监听器不会被执行,在想要确保当前事件处理器是唯一被调用的处理器时非常有用
console.log('inner 捕获')
}, true)
inner.addEventListener('click', () => {
console.log('inner 冒泡')
}, false)
</script>
</body>

</html>

当点击inner元素时,以下是事件的传播和冒泡过程:

事件捕获阶段(从rootinner):

  1. root的捕获事件监听器被触发,控制台输出:”root 捕获”
  2. outer的捕获事件监听器被触发,控制台输出:”outer 捕获”
  3. inner的捕获事件监听器被触发,控制台输出:”inner 捕获”

目标阶段(在inner上):

在目标元素上,即inner元素上,如果存在任何事件监听器,它们将按照它们被添加到元素上的顺序被触发。在这段代码中,由于inner同时有捕获和冒泡的监听器,且捕获阶段的监听器已经被触发过,所以下一个是冒泡阶段的监听器:

  • inner的冒泡事件监听器被触发,控制台输出:”inner 冒泡”

事件冒泡阶段(从innerroot):

  1. outer的冒泡事件监听器被触发,控制台输出:”outer 冒泡”
  2. root的冒泡事件监听器被触发,控制台输出:”root 冒泡”

总结起来,点击inner元素时触发的事件按以下顺序传播:

  • 捕获阶段:root -> outer -> inner
  • 目标阶段:inner(捕获阶段的监听器已经执行,如果有的话,执行目标阶段的监听器)
  • 冒泡阶段:inner -> outer -> root

控制台的输出将会是这样的顺序:

1
2
3
4
5
6
root 捕获
outer 捕获
inner 捕获
inner 冒泡
outer 冒泡
root 冒泡

每个输出对应一个在相应的DOM元素上触发的事件监听器。

没有事件传播机制的事件

不是所有的DOM事件都有事件传播机制,也就是说,不是所有的事件都会经历捕获和冒泡阶段。有一些事件是不会冒泡的,它们只在目标元素上触发。以下是一些常见的不冒泡的事件类型:

  1. focus:当元素获得焦点时触发。
  2. blur:当元素失去焦点时触发。
  3. load:在对象加载完成时触发,如windowdocumentimg等元素。
  4. unload:当一个页面或一个图像被卸载时触发。
  5. resize:当窗口或框架被重新调整大小时触发。
  6. scroll:当元素的滚动条被滚动时触发(虽然 scroll 事件在现代浏览器中会冒泡,但在较旧的浏览器中可能不会)。
  7. mouseenter:当鼠标指针移动到元素上时触发,不冒泡。
  8. mouseleave:当鼠标指针移出元素时触发,不冒泡。

请注意,虽然focusblur事件不冒泡,但在现代浏览器中,可以使用focusinfocusout事件替代,这两个事件具有和focusblur相似的功能,但它们是会冒泡的。

此外,实际的事件传播行为可能因浏览器的实现而异,因此最佳实践是查阅最新的浏览器文档以获取准确信息。

事件委托

事件委托的模式:在body元素上监听了点击(click)事件,并根据事件的目标元素(ev.target)的id,判断具体是哪个元素被点击。这里的事件监听器是绑定在body元素上的,而不是每个子元素上,这样做有几个好处:

  1. 节省内存:不必为每个元素分别添加事件监听器,只需在共同的祖先元素上添加一个监听器即可。

  2. 动态元素处理:即使在事件监听器添加后,新的元素被添加到DOM中,它们也无需额外的事件绑定,因为点击事件会冒泡到body元素并被相同的监听器处理。

  3. 简化管理:有助于简化事件监听器的管理,因为只需要在一个地方添加和移除监听器,而不是在每个子元素上分别操作。

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
<!--
* @Description:
* @Author: xiuji
* @Date: 2024-01-11 15:40:55
* @LastEditTime: 2024-01-16 09:21:14
* @LastEditors: Do not edit
-->
<!DOCTYPE html>
<html lang="zh-CN">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}

body {
width: 100%;
height: 100%;
}

.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

#root {
width: 200px;
height: 200px;
background-color: #ccc;
}

#outer {
width: 100px;
height: 100px;
background-color: #999;
}

#inner {
width: 50px;
height: 50px;
background-color: #666;
}

#target {
width: 200px;
height: 200px;
background-color: skyblue;
position: absolute;
top: calc(50% - 200px);
left: 50%;
transform: translate(-50%, -50%);
}
</style>
</head>

<body>
<div id="root" class="center">
<div id="outer" class="center">
<div id="inner" class="center"></div>
</div>
</div>
<script>
const body = document.body
const target = document.createElement('div') // 动态添加的DOM元素
target.id = 'target'
body.appendChild(target)
body.addEventListener('click', (ev) => {
const target = ev.target
if (target.id === 'root') {
console.log('root')
} else if (target.id === 'outer') {
console.log('outer')
} else if (target.id === 'inner') {
console.log('inner')
} else if (target.id === 'target') {
console.log('target')
}
},true)
</script>
</body>

</html>

在这段代码中,事件委托的机制使得无论是现有的rootouterinner元素,还是动态创建的target元素,只要它们被点击,都只用一个统一的事件监听器就能处理。这在动态内容和大型应用中非常有用,可以有效地处理用户交云,并减少性能开销。

例如,当用户点击页面上的任何一个元素(rootouterinnertarget)时,控制台将打印出相应元素的id,因为事件会从被点击的元素冒泡到body,在那里被事件监听器捕获并判断ev.targetid来确定具体是哪个元素被点击了。这使得可以在不直接绑定事件到每个元素上的情况下,依然能够响应其事件。

合成事件原理

工作原理

React的合成事件工作原理大致如下:

  1. 事件监听:React并没有直接将事件处理器绑定到真实的DOM节点上,而是将所有事件处理器绑定到文档的根节点。当事件发生时(比如点击或者输入),它会冒泡到根节点。

  2. 事件封装:React监听到原生事件后,会创建一个合成事件的实例。这个合成事件包含了原生事件的所有信息,并且统一了不同浏览器间的差异。

  3. 事件处理:然后React会根据内部映射来确定事件应该被哪个组件处理,并调用在那个组件上定义的props中的事件处理函数。

  4. 事件池:为了优化性能,React使用了事件对象池。合成事件被处理后,其对象会被回收到池中,以便于后续重用。这意味着合成事件是临时的,它们的属性值在事件回调函数执行完毕后会被清除。

事件处理顺序

当一个原生事件触发时,比如一个click事件,React的事件委托机制会捕获到这个事件,并按以下步骤处理:

  1. 检查是否有对应的React事件处理器。
  2. 创建一个合成事件对象,并将原生事件的属性复制到该对象中。
  3. 根据React的元素和组件树结构,决定哪些组件应当接收事件。
  4. 按照组件树结构从顶层到底层调用事件处理器。
  5. 回收合成事件对象到事件池中,以供后续事件重用。

这个系统使得事件处理在React中更加高效和一致,同时还简化了跨浏览器的兼容性问题。

总结

React 的合成事件系统是一种高效且跨浏览器的事件处理机制。在 React 中,事件处理不是通过直接在 DOM 元素上绑定事件监听器来实现的,而是通过为组件设置特殊的合成事件属性(如 onClickonMouseOver)来完成。这些属性对应于 React 内部的事件处理函数。

当一个事件发生时,它会在 DOM 树上按照标准的捕获和冒泡阶段传播。React 采用事件委托的方式,在根DOM元素(通常是页面上的 #root 容器)上为所有支持的事件类型添加了单个的事件监听器。一旦事件冒泡到 #root,React 就会根据内部维护的映射来决定哪个组件的哪个事件处理器应当被调用,并创建一个封装了原生事件的合成事件对象。

这个合成事件对象提供了一个统一的API,使开发人员不必担心不同浏览器之间的差异。React还实现了事件池机制以提高性能,即事件处理完毕后,合成事件对象可能会被回收以供后续事件重用。这就要求开发者不应在异步代码中引用这些事件属性,因为它们可能在事件回调执行后已经被清除。

React 也允许事件处理在捕获阶段发生,而不仅仅是在冒泡阶段。这可以通过为事件处理器添加 Capture 后缀来实现,如 onClickCapture。这种设计不仅优化了事件处理的性能,还提供了一致性和可扩展性,使开发者能够构建可靠的交互式用户界面。