React 的合成事件机制
一、什么是 React 合成事件
React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
<button onClick={handleClick}>按钮</button>
进行浏览器兼容,实现更好的跨平台;
React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。
避免垃圾回收;
事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发时,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)
方便事件统一管理和事务机制。
二、合成事件与原生事件的区别
1、事件命名方式不同
原生事件命名为纯小写(onclick, onblur),而 React 合成事件采用小驼峰式(camelCase)命名。
// 原生事件绑定方式
<button onclick="handleClick()">按钮</button>
// React 合成事件绑定方式
<button onClick={handleClick}>按钮</button>
2、事件处理函数写法不同
原生事件中事件处理函数用字符串表示,而 React 合成事件用 JSX 语法,用 {} 包裹事件处理函数。
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮</button>
// React 合成事件 事件处理函数写法
<button onClick={handleClick}>按钮</button>
3、阻止默认行为方式不同
原生事件中,可以通过 return false
的方式来阻止默认行为,而 React 合成事件需要显式使用 preventDefault()
方法来阻止。
// 原生事件阻止默认行为方式
<a
href="xx.com"
onclick="return false"
>
阻止原生事件
</a>
// React 事件阻止默认行为方式
const handleClick = (e) => {
e.preventDefault()
}
const clickElement = (
<a href="xx.com" onClick={handleClick}>
阻止原生事件
</a>
)
三、合成事件的执行顺序与冒泡阻止
1、合成事件的执行顺序
React 合成事件以事件委托(Event Delegation)的方式绑定在组件最上层,并在组件卸载(unmount)阶段自动销毁绑定的事件。
举个例子:
import React from 'react'
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 事件监听')
})
}
handleClickFather = () => {
console.log('React 合成事件:父元素事件监听')
}
handleClickChild = () => {
console.log('React 合成事件:子元素事件监听')
}
render() {
return (
<div ref={this.parentRef} onClick={this.handleClickFather}>
<div ref={this.childRef} onClick={this.handleClickChild}>
分析事件执行顺序
</div>
</div>
)
}
}
export default App
// 输出顺序为:
// 原生事件:子元素 DOM 事件监听
// 原生事件:父元素 DOM 事件监听
// React 合成事件:子元素事件监听
// React 合成事件:父元素事件监听
// 原生事件:document DOM 事件监听
可以得出以下结论:
- React 合成事件的监听器统一注册在 document 上,且仅有冒泡阶段,所以原生事件的监听器响应总是比合成事件的监听器早;
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;
- 所以当各事件同时存在时,会先执行原生事件,然后处理 React 事件,最后执行 document 上挂载的事件。
React 17 之前是将合成事件委托在 document
元素上的,17 之后将合成事件委托在 root
元素上了。这样做的好处是页面上可以共存多个 react 版本了,避免多个版本的 React 共存时事件系统发生冲突。
2、合成事件的冒泡阻止
React 合成事件阻止不同时间段的冒泡行为,需要对应使用不同的方法,对应如下:
阻止合成事件间的冒泡,可以用
e.stopPropagation()
e.stopPropagation()
只能阻止合成事件间冒泡(即下层的合成事件),不会冒泡到上层的合成事件,事件本身还都是在 document 上执行,所以最多只能阻止 document 事件不冒泡到 window 上。阻止合成事件与最外层 document 上的事件间的冒泡,可以用
e.nativeEvent.stopImmediatePropagation()
阻止合成事件与除最外层 document 上的原生事件上的冒泡,可以通过判断
e.target
来避免。
四、合成事件的常见问题
1、合成事件的 this 指向问题
React JSX 回调函数中的 this 经常会出问题,Class 中的方法不会默认绑定 this,就会出现下面 this.funName 值为 undefined
的情况:
class App extends React.Component {
handleClickChild = () => {
console.log('React 事件')
}
handleClick() {
console.log(this.handleClickChild) // undefined
}
render() {
return <div onClick={this.handleClick}>React this 指向问题</div>
}
}
export default App
解决方法一
在 constructor 中使用 bind 方法绑定 this:
class App extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
// ...
}
export default App
解决方法二
将需要使用 this 的方法改写为使用箭头函数定义:
class App extends React.Component<any, any> {
handleClick = () => {
console.log(this.handleClickChild)
}
// ...
}
export default App
或在回调函数中使用箭头函数:
class App extends React.Component {
// ...
handleClick() {
console.log(this.handleClickChild)
}
render() {
return <div onClick={() => this.handleClick()}>React this 指向问题</div>
}
}
export default App
2、合成事件的参数传递问题
遍历列表时,常常要向事件传递额外参数,如 id 等,来指定需要操作的数据,React 中有 2 种方式向事件传参:
const List = [1, 2, 3, 4]
class App extends React.Component {
// ...
clickFun(id) {
console.log('当前点击:', id)
}
render() {
return (
<div>
<h1>第一种:通过 bind 绑定 this 传参</h1>
{List.map((item) => (
<div onClick={this.clickFun.bind(this, item)}>按钮:{item}</div>
))}
<h1>第二种:通过箭头函数绑定 this 传参</h1>
{List.map((item) => (
<div onClick={() => this.clickFun(item)}>按钮:{item}</div>
))}
</div>
)
}
}
export default App
这两种方式是等价的:
- 第一种通过
Function.prototype.bind
实现; - 第二种通过箭头函数实现。