基本使用
React
框架自身实现了一套事件处理机制,它的基本用法和DOM
事件很相似。例如,给某个react
元素绑定点击事件:
- 事件类型采用小驼峰命名法,因此是
onClick
,而不是onclick
,其他事件类型相同。 - 直接将函数的声明当成事件句柄传递
我们将它的这套事件处理机制称之为SyntheticEvent
,即合成事件
Synthetic Event
支持的事件类型
React
支持的事件类型如下:
事件流
React
中,默认的事件传播方式为冒泡:
import React, { Component } from "react"import ReactDOM from "react-dom"const styles = { child: { width: "100px", height: "100px", backgroundColor: "red" }, parent: { width: "150px", height: "150px", backgroundColor: "blue" }, ancestor: { width: "200px", height: "200px", backgroundColor: "black" }}class App extends Component { render() { return ({ console.log("ancestor") }} style={styles.ancestor}>); }}ReactDOM.render({ console.log("parent"); }} style={styles.parent}>{ console.log("child"); }} style={styles.child}>, document.querySelector("#root"));
在该示例中,3
个div
嵌套显示,并且每个元素上均绑定onClick
事件:
当用户点击红色区域的div
元素时,可以看到,控制台先后输出了child -> parent -> ancestor
,这是因为在React
的事件处理系统中,默认的事件流就是冒泡,如果说我们希望以捕获的方式来触发事件的话,可以使用onClickCapture
来绑定事件,也就是在事件类型后面加一个后缀Capture
:
import React, { Component } from "react"import ReactDOM from "react-dom"const styles = { child: { width: "100px", height: "100px", backgroundColor: "red" }, parent: { width: "150px", height: "150px", backgroundColor: "blue" }, ancestor: { width: "200px", height: "200px", backgroundColor: "black" }}class App extends Component { render() { return ({ console.log("ancestor") }} style={styles.ancestor}>); }}ReactDOM.render({ console.log("parent"); }} style={styles.parent}>{ console.log("child"); }} style={styles.child}>, document.querySelector("#root"));
这时,还是点击红色区域的div
,就可以看到事件流是从ancestor -> parent -> child
传播了。
事件委托
在合成事件系统中,所有的事件都是绑定在document
元素上,即,虽然我们在某个react
元素上绑定了事件,但是,最后事件都委托给document
统一触发。
在合成事件中只能阻止合成事件中的事件传播
我们在红色区域的div
里,也就是最里层的那个元素上,使用e.stopPropagation()
方法来阻止事件流的传播:
{ console.log("ancestor") }} style={styles.ancestor}>{ console.log("parent"); }} style={styles.parent}>{ console.log("child"); e.stopPropagation(); }} style={styles.child}>
点击红色区域的,我们可以看到控制台上只输出了child
,说明这时已经成功阻止了冒泡。执行流程如下:
从图中我们可以看到,react
阻止的事件流,并没有阻止真正DOM
元素的事件触发,当红色div
元素被点击时,真正的元素还是按照冒泡的方式,层层将事件交给上级元素进行处理,最后事件传播到docuement
,触发合成事件,在合成事件中,child
触发时,e.stopPropagation();
被调用,合成事件中的事件被终止。因此,合成事件中的stopPropagation
无法阻止事件在真正元素上的传递,它只阻止合成事件中的事件流。相反,如果我们在红色的div
上,绑定一个真正的事件,那么,合成事件则会被终止。
事件对象
在SyntheticEvent
中,我们依然可以获取到事件发生时的event
对象:
import React, { Component } from "react"import ReactDOM from "react-dom"const styles = { "DEBUG_DISPLAY": { width: "150px", height: "150px", backgroundColor: "red" }}class App extends Component { state = { x: 0, y: 0 } render() { return (); }}ReactDOM.render({ console.log(e) }} > x: {this.state.x},y: {this.state.y}, document.querySelector("#root"));
以上示例中,我们给div
元素绑定了一个click
事件,在用户点击时,在控制台输出event
对象:
接下来将用户点击时的坐标在div
元素中显示出来,可以通过clientX
和clientY
来访问:
{ this.setState({ x: e.clientX, y: e.clientY }); }} > x: {this.state.x},y: {this.state.y}
合成事件中的event
对象,并不是原生的event
,只是说,我们可以通过它获取到原生event
对象上的某些属性,比如以上示例中的clientX
和clientY
。而且,对于这个event
对象,在整个合成事件中,只有一个,被全局共享,也就是说,当这次事件调用完成之后,这个event
对象会被清空,等待下一次的事件触发,因此,我们无法在异步的操作中获取到event
:
{ setTimeout(() => { this.setState({ x: e.clientX, y: e.clientY }); }, 1000); }} > x: {this.state.x},y: {this.state.y}
当用户点击div
,1
秒之后获取点击时的坐标,这时,可以看到控制台抛出错误:
在异步操作中想要获取event
对象中的数据,在事件发生时就需要将数据通过变量保存下来:
{ const { clientX, clientY } = e; setTimeout(() => { this.setState({ x: clientX, y: clientY }); }, 1000); }} > x: {this.state.x},y: {this.state.y}
混合使用
react
鼓励我们使用合成事件,但是,在某些需求中,还是需要通过原生事件来进行处理,这时,就涉及到合成事件和原生事件的混合使用,例如以下示例:
import React, { Component } from "react"import ReactDOM from "react-dom"class App extends Component { state = { isShow: "none" } render() { return (); }}ReactDOM.render(, document.querySelector("#root"));
在这个示例中,我们提供一个按钮和一个div
元素,当用户点击按钮时,显示div
,当点击页面其他区域时,则隐藏。以上代码已经实现点击按钮显示div
的功能:
要实现 点击其他区域隐藏div
元素 的功能,需要将事件绑定在document
元素上,接下来,在compnentDidMount
生命周期函数中,来绑定该事件:
class App extends Component { state = { ... } componentDidMount() { document.addEventListener("click", e => { this.setState({ isShow: "none" }); }) } render() { ... }}
当点击按钮时,isShow: "block"
,当点击其他区域时,isShow: "none"
。这时我们发现,点击按钮时,div
显示不出来了。
这个现象的原因是,实际上,在document
元素身上,现在已经存在2
个click
事件,一个是合成事件绑定的click
,另外一个是我们自己添加的监听器。当用户点击按钮时,synthetic
中的click
首先被触发,这时,isShow
状态被设置成block
,页面元素被显示出来,紧跟着,native
中的click
事件被触发,又把isShow
的状态改为none
,div
元素又被隐藏了起来。
处理方法:
import React, { Component } from "react"import ReactDOM from "react-dom"class App extends Component { state = { isShow: "none" } button = React.createRef(); componentDidMount() { document.addEventListener("click", e => { // 当 native 事件被触发时,我们判断一下当前目标元素是否为 button, // 如果不是点击的按钮,则就意味着将元素隐藏 if (e.target !== this.button.current) { this.setState({ isShow: "none" }); } }) } render() { return (); }}ReactDOM.render(, document.querySelector("#root"));