Skip to main content

React 的合成事件机制

一、什么是 React 合成事件

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。

<button onClick={handleClick}>按钮</button>
React 合成事件的作用
  • 进行浏览器兼容,实现更好的跨平台;

    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 实现;
  • 第二种通过箭头函数实现。