react 事件原理
2023-06-13 21:37:08
React 中所有触发的事件,都是自己在其内部封装了一套事件机制。目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。
事件合成
在 React17 之前,React 是把事件委托在 document 上的,React17 及以后版本不再把事件委托在 document 上,而是委托在挂载的容器上。React 合成事件采用的是事件冒泡机制,当在某具体元素上触发事件时,等冒泡到顶部被挂载事件的那个元素时,才会真正地执行事件。
而原生事件,当某具体元素触发事件时,会立刻执行该事件。因此若要比较事件触发的先后时机时,原生事件会先执行,React 合成事件会后执行。
React 的事件不是绑定在元素上的,而是统一绑定在顶部容器上,在 v17 之前是绑定在 document 上的,在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用(微前端)。
绑定事件并不是一次性绑定所有事件,比如发现了 onClick 事件,就会绑定 click 事件,比如发现 onChange 事件,会绑定 [blur,change ,focus ,keydown,keyup] 多个事件。
看下面这段代码的执行顺序
class App extends React.Component {
constructor(props) {
super(props)
this.parentRef = React.createRef()
this.childRef = React.createRef()
}
componentDidMount() {
console.log('React componentDidMount!')
this.parentRef.current?.addEventListener('click', () => {
console.log('原生事件:父元素 DOM 事件监听!')
})
this.childRef.current?.addEventListener('click', () => {
console.log('原生事件:子元素 DOM 事件监听!')
})
document.addEventListener('click', e => {
console.log('原生事件:document DOM 事件监听!')
})
}
parentClickFun = () => {
console.log('React 事件:父元素事件监听!')
}
childClickFun = () => {
console.log('React 事件:子元素事件监听!')
}
render() {
return (
<div ref={this.parentRef} onClick={this.parentClickFun}>
<div ref={this.childRef} onClick={this.childClickFun}>
分析事件执行顺序
</div>
</div>
)
}
}
export default App
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
输出顺序为
原生事件:子元素 DOM 事件监听!
原生事件:父元素 DOM 事件监听!
React 事件:子元素事件监听!
React 事件:父元素事件监听!
原生事件:document DOM 事件监听!
2
3
4
5
可以得出结论
- React 所有事件都挂载在 document 对象上
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 所以会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用 e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡,用 e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与除最外层 document 上的原生事件上的冒泡,通过判断 e.target 来避免
事件插件机制
React 有一种事件插件机制,比如上述 onClick 和 onChange ,会有不同的事件插件 SimpleEventPlugin,ChangeEventPlugin 处理。
const registrationNameModules = {
onBlur: SimpleEventPlugin,
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin,
onMouseEnter: EnterLeaveEventPlugin,
onMouseLeave: EnterLeaveEventPlugin
}
2
3
4
5
6
7
8
9
registrationNameModules 记录了 React 事件(比如 onBlur )和与之对应的处理插件的映射,比如上述的 onClick ,就会用 SimpleEventPlugin 插件处理,onChange 就会用 ChangeEventPlugin 处理。应用于事件触发阶段,根据不同事件使用不同的插件。
const registrationNameDependencies = {
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: [
'blur',
'change',
'click',
'focus',
'input',
'keydown',
'keyup',
'selectionchange'
],
onMouseEnter: ['mouseout', 'mouseover'],
onMouseLeave: ['mouseout', 'mouseover']
}
registrationNameDependencies 对象保存了 React 事件和原生事件对应关系,这就解释了为什么上述只写了一个 onChange ,会有很多原生事件绑定在 document 上。在事件绑定阶段,如果发现有 React 事件,比如 onChange ,就会找到对应的原生事件数组,逐一绑定。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
总结
- React 上注册的事件最终会绑定在 document 这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都- 绑定在 document 上,其他节点没有绑定事件)
- React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件 SyntheticEvent