React 中的事件
这里的事件,指的是 React 内部封装 DOM 组件中的事件,如 onClick, onFocus等
,而非我们自己通过 props
传递的属性,并在子组件中手动触发的事件
看个栗子
我们可能会有这么个 需求
:需要做一个弹窗打开/关闭
的功能,当点击 button
的时候打开;打开的情况下,点击弹窗 区域
外,就需要关闭弹窗
简单嘛,我们在 button 上注册一个事件,点击则展示 Modal,在 document.body
上注册一个关闭 Modal 的事件,在 Modal 内容区阻止事件冒泡不就可以了吗?!说干就干,示例代码如下:
export default class Modal extends PureComponent {
state = { showModal: false }
componentDidMount() {
document.body.addEventListener('click', this.handleClose, false)
}
componentWillUnmount() {
document.body.removeEventListener('click', this.handleClose, false)
}
handleClose = () => {
this.setState({ showModal: false })
}
handleShow = () => {
this.setState({ showModal: true })
}
stopBubble = e => {
e.stopPropagation()
}
render() {
return (<div>
<button onClick={this.handleShow}>Show Modal</button>
{this.state.showModal && (
<div onClick={this.stopBubble}>Modal Content</div>
)}
</div> )
}
}
如果 React 版本 < 17
,那么你就会发现表现结果跟上面实现的想法并不一致,点击 Modal Content 区同样会关闭 Modal,也就是说 stopBubble
函数并未完成按照咱的预期去阻止事件冒泡的功能;但如果 React 版本 >= 17
,那么上面的方案确实能够生效了
真实 DOM 事件与 React 事件调用顺序
比如下面这个栗子:
import React, { PureComponent } from 'react'
import {createPortal} from 'react-dom'
function Comp() {
return <h1
style={{ color: "#008c8c" }}
onClick={() => {
console.log('H1 事件')
}}>Comp</h1>
}
/* 插槽组件 */
const PortalComp = () => {
return createPortal(<Comp />, document.body)
}
export default class Test extends PureComponent {
componentDidMount() {
document.addEventListener('click', () => {
console.log('document 事件')
})
const dom = document.querySelector("#root")
dom.addEventListener('click', () => {
console.log('真实的 DOM root div 被点击')
})
}
handleClick = () => {
console.log('red div 事件')
// e.stopPropagation() // 阻止冒泡
}
render() {
return (
<div
style={{
width: 100,
height: 100,
backgroundColor: "#f40"
}}
onClick={this.handleClick}
>
<PortalComp /> {/* 插槽组件 */}
</div>
)
}
}
我这里是 react 17.0.0 版,所以点击元素后的打印结果及说明:
1. 点击 h1 元素
H1 事件
red div 事件
document 事件
说明:h1 元素真实结构是 body 的直接子元素,但是也可以看到可以通过事件冒泡,触发其虚拟 DOM 树结构的父元素 (红色 div) 的点击事件以及 document 绑定的点击事件,但 没有触发
root 节点绑定的真实 DOM 的点击事件 (这个表现说明它既不遵循 虚拟 DOM 树
冒泡机制,也不遵循 真实 DOM 树
的冒泡机制)
2. 点击红色 div 元素
red div 事件
真实的 DOM root div 被点击
document 事件
说明:红色 div 元素在结构是 root div 的直接子元素,可以看到可以通过事件冒泡,能够触发 root 节点绑定的真实 DOM 的点击事件以及 document 绑定的点击事件
到这里,其实有点诧异,为何点击 h1 元素没有触发 root 节点绑定的点击事件?!
上面的栗子改造一下
import React, { PureComponent } from 'react'
function Comp() {
return <h1
style={{ color: "#008c8c" }}
onClick={e => {
console.log('H1 事件')
// e.stopPropagation() // 阻止事件冒泡
}}>Comp</h1>
}
export default class Task extends PureComponent {
componentDidMount() {
document.addEventListener('click', () => {
console.log('document 事件')
})
const dom = document.querySelector("#root")
dom.addEventListener('click', () => {
console.log('真实的 DOM root div 被点击')
})
}
handleClick = () => {
console.log('red div 事件')
}
render() {
return (
<div
style={{
width: 100,
height: 100,
backgroundColor: "#f40"
}}
onClick={this.handleClick}
>
<Comp />
</div>
)
}
}
这个栗子中,我没有使用 Portal 插槽去改变组件的渲染位置,在 Comp 组件
中 不阻止
事件冒泡时,点击 h1 元素就会按照我们预想的结果一样去冒泡,并能触发真实 DOM 事件,依次打印:(1) H1 事件; (2) red div 事件; (3) 真实的 DOM root div 被点击; (4) document 事件
;
当我解开阻止事件冒泡 // e.stopPropagation()
的注释时,点击 h1 就只会打印:(1) H1 事件; (2) 真实的 DOM root div 被点击;
(这里发现,document 注册的真实 DOM 事件也被阻止了;React v17 版不是将事件委托的节点转移到 root 根节点了吗?!为何还能阻止 document 上的事件?) 问题~~
事件说明
React 根据 W3C 规范定义了合成事件 (SyntheticEvent
),我们就无需担心跨浏览器的兼容性问题
React 出于性能与事件管理方面的考量,在之前的 React 版本中 (v17 之前)
-
会将在 JSX 中注册的事件收集到 document 上 (即通过事件委托,在 document 上注册事件,等触发事件时,则按虚拟 DOM 树的结构进行事件触发机制去分发事件);
-
几乎所有的事件处理,均在 document 的事件中处理
- 比如
onFocus
等事件不会冒泡的事件,就不做委托,直接在元素上监听 - 一些 document 上没有的事件,也直接在元素上监听 (如:audio、video标签的事件等)
- 比如
-
在 document 中的事件处理,会根据虚拟 DOM 树的结构完成事件函数的调用,默认是冒泡机制 (事件捕获要通过类似
onClickCapture
的方式注册) -
React 的事件参数,并非真实的事件参数,而是 React 合成的一个对象 (
SyntheticEvent
)- 通过调用
e.stopPropagation()
阻止事件冒泡 (仅阻止 React 事件) - 通过
e.nativeEvent
可以得到真实的 DOM 事件对象 (不过很少会用到) - 为了提高效率,React 使用事件对象池来处理事件对象 (即事件对象会重用)
- 通过调用
-
在 React 17 之后,事件委托的节点就转移到了渲染的根节点上,而且也帮我们解决了此类关于事件冒泡的问题 (本文测试用例则说明,对于使用
ReactDOM.createPortal
创建的组件,表现上略有差异)
注意点
-
若给真实 DOM 注册事件,并阻止冒泡,则很有可能导致 React (JSX) 中注册的相关事件无法触发
-
若给真实 DOM 注册事件,它会先于 React 事件执行 (即通过
onClick
和dom.addEventListener
绑定的事件,真实 DOM 事件会先执行;因为这个元素被点击时,真实 DOM 事件会很快找到,而 React 绑定的事件则需要去找到事件委托的元素,再去调用当前点击元素绑定的事件函数) -
通过 React 事件阻止事件冒泡,无法阻止真实 DOM 事件的冒泡 (如上面改造的栗子的结果)
-
可以使用
e.nativeEvent.stopImmediatePropagation()
去阻止 document 上剩余的事件处理程序的运行 (当我们在使用某些第三方库,在这个库有可能使用了一些事件处理,也对 document 绑定过点击事件,如:document.addEventListener("click", handler)
) -
在事件处理程序中,不要异步使用事件对象
e
;如果一定有异步使用的需求,则需要调用e.persist()
函数持久化保存此事件对象 (代价自然是损耗效率的)