概述
Web应用中,事件处理是重要的一环,事件处理将用户的操作行为转换为相应的逻辑执行或界面更新。在React中,处理事件响应的方式有多种,本文将详细介绍每一种处理方式的用法、使用场景和优缺点。
如果原生DOM有一个监听事件,我们可以如何操作?
- 获取DOM元素,添加监听事件
- 在HTML中直接进行onClick绑定
使用匿名函数
点击Button的事件响应函数是一个匿名函数,这应该是最常见的处理事件响应的方式了。这种方式的好处是,简单直接。哪里需要处理事件响应,就在哪里定义一个匿名函数处理。代码1中的匿名函数使用的是箭头函数,我们也可以不使用箭头函数:
虽然代码2的运行效果和代码1相同,但实际项目中很少见到代码2的这种写法。这是因为箭头函数解决了this绑定的问题,可以将函数体内的this绑定到当前对象,而不是运行时调用函数的对象。如果响应函数中需要使用this.state,那么代码2就无法正常运行了。所以项目中一般直接使用箭头函数定义的匿名函数作为事件响应。
使用匿名函数的缺点是:当事件响应逻辑比较复杂时,匿名函数的代码量会很大,会导致render函数变得臃肿,不容易直观地看出组件最终渲染出的元素结构。另外,每次render方法调用时,都会重新创建一个匿名函数对象,带来额外的性能开销,当组件的层级越低时,这种开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的render方法。当然,在大多数情况下,这点性能损失是可以不必在意的。
使用组件方法
代码如下:
点击Button的事件响应函数是组件的方法:handleClick。这种方式的好处是:每次render方法的调用,不会重新创建一个新的事件响应函数,没有额外的性能损失。但是,使用这种方式要在构造函数中为作为事件响应的方法(handleClick),手动绑定this: this.handleClick = this.handleClick.bind(this)
,这是因为ES6 语法的缘故,ES6 Class 的方法默认不会把this绑定到当前的实例对象上,需要我们手动绑定。每次都手动绑定this是不是有点繁琐?好吧,让我们来看下一种方式。
使用属性初始化语法
使用ES7的属性初始化语法( property initializers ),代码可以这样写:
这样一来,再也不用手动绑定this了。但是你需要知道,这个特性还处于试验阶段,默认是不支持的。如果你是使用官方脚手架create-react-app 创建的应用,那么这个特性是默认支持的。你也可以自行在项目中引入babel的transform-class-properties插件获取这个特性支持。
事件响应函数的传参问题
事件响应函数默认是会被传入一个事件对象Event作为参数的。如果想传入其他参数给响应函数应该怎么办呢?
使用第一种方式的话很简单,直接使用新参数:
onClick的响应函数中,方法体内可以直接使用新的参数item。
使用第二种方式的话,可以把绑定this的操作延迟到render中,在绑定this的同时,绑定额外的参数:
使用第三种方式,解决方案和第二种基本一致:
不过这种方式就有点鸡肋了,因为虽然你不需要通过bind函数绑定this,但仍然要使用bind函数来绑定其他参数。
关于事件响应函数,还有一个地方需要注意。不管你在响应函数中有没有显式的声明事件参数Event,React都会把事件Event作为参数传递给响应函数,且参数Event的位置总是在其他自定义参数的后面。例如,在代码6和代码7中,handleClick
的参数中虽然没有声明Event参数,但你依然可以通过arguments[1]
获取到事件Event对象。
总结一下,三种事件处理的方式,第一种有额外的性能损失;第二种需要手动绑定this,代码量增多;第三种用到了ES7的特性,目前并非默认支持,需要Babel插件的支持,但是写法最为简洁,也不需要手动绑定this。一般推荐使用第二种和第三种方式。
这里呢,只是对事件处理做了一个简要的概述,下面我们将详细讲解有关事件操作的方法
注册事件
- React注册事件与DOM的事件语法非常像
- 语法
on+事件名={事件处理程序}
比如onClick={this.handleClick}
- 注意:React事件采用驼峰命名法,比如
onMouseEnter
,onClick
注意事项:
- 在 react 中时间的命名采用的是小驼峰形式(例如:onClick),而不是纯小写
- 我们需要通过{}传入一个事件处理函数,这个函数会在事件发生时被执行
函数式组件中绑定事件
import React from "react";
// 方式1
// function handlerClick() {
// console.log(1);
// }
// 方式2:推荐
// const handlerClick = () => {
// console.log(1);
// }
export default function funHello() {
// function handlerClick() {
// console.log(1);
// }
const handlerClick = () => {
console.log(1);
};
return (
<div>
<button onClick={handlerClick}>点击</button>
</div>
);
}
类组件中绑定事件
import React, { Component } from 'react'
export default class classHello extends Component {
constructor(props){
super(props)
this.state = {
message: 'hello'
}
}
// 方式1:利用箭头函数--推荐
handlerMsg = () => {
this.setState({
message: 'hello react'
})
}
// 方式2:利用bind等方式改变this指向
// handlerMsg() {
// console.log(this);
// }
render() {
const { message } = this.state
return (
<div>
<p>{message}</p>
<button onClick={this.handlerMsg}>更改</button>
{/* <button onClick={this.handlerMsg.bind(this)}>更改</button> */}
</div>
)
}
}
事件对象
- 可以通过事件处理程序的参数获取到事件对象
function handleClick(e) {
e.preventDefault()
console.log('事件对象', e)
}
<a onClick={this.handleClick}>点我,不会跳转页面</a>
// 或者
handleClick = (e) => {
e.preventDefault()
console.log('事件对象', e)
}
<a onClick={this.handleClick}>点我,不会跳转页面</a>
handlerMsg(e) {
console.log(e, this);
}
<button onClick={(e) => this.handlerMsg(e)}>更改</button>
handlerMsg(e, name, age) {
console.log(e, this, name, age);
}
<button onClick={(e) => this.handlerMsg(e, 'zs', 15)}>更改</button>
this指向问题
在jsx中事件处理函数执行时,会导致this丢失,我们可以用bind修改回来
事件处理程序中的this指向的是undefined
render方法中的this指向的而是当前react组件。只有事件处理程序中的this有问题
- 在类中直接定义一个函数,并且将这个函数绑定到元素的onClick事件上,当前这个函数的this指向的是谁呢?---undefined
- 默认情况下是undefined
- 比如
<button onClick={this.handlerMsg}>更改</button>
,这里的this.handlerMsg
当点击之后并不是直接去调用函数,而是进行一个指向操作,react代码在进行编译的时候通过bable,默认会将代码转换成严格模式,在严格模式下的this指向是undefined,因此默认情况下也就是undefined - 我们在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的this
- 此时我们就需要在传入函数时,给这个函数直接绑定this
- 我们可以利用 bind 将其修改回来,比如:
<button onClick={this.changeText.bind(this)}>改变文本</button>
- 当然也可以利用箭头函数来解决
import React, { Component } from 'react'
export default class classHello extends Component {
constructor(props){
super(props)
this.state = {
message: 'hello'
}
}
// 方式1:利用箭头函数--推荐
handlerMsg = () => {
this.setState({
message: 'hello react'
})
}
// 方式2:利用bind等方式改变this指向
// handlerMsg() {
// this.setState({
// message: 'hello react'
// })
// }
// 方式3:利用声明式
//function handlerMsg() {
// this.setState({
// message: 'hello react'
// })
// }
render() {
return (
<div>
<p>{this.state.message}</p>
<button onClick={this.handlerMsg}>更改</button>
{/* <button onClick={this.handlerMsg.bind(this)}>更改</button> */}
</div>
)
}
}
this指向问题解决方案
方案1:箭头函数
方案2:bind修改this指向
方案3:类实例方法
使用箭头函数
import React, { Component } from 'react'
export default class ClickClass extends Component {
constructor() {
super();
this.msg = '消息';
}
// 箭头函数
click = () => {
console.log('类组件点击事件');
console.log(this.msg);
}
render() {
return (
<div>
<div onClick={this.click}>类组件点击事件</div>
</div>
)
}
}
箭头函数的特点:自身没有this,访问的是外部的this
缺点:会把大量的js处理逻辑放到JSX中,将来不容易维护
直接传入箭头函数
推荐
更有利于传参
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick() {
console.log(this.state.msg)
}
render() {
return (
<div>
<button onClick={() => {this.handleClick()}}>点我</button>
</div>
)
}
}
缺点:把大量的js逻辑写在了JSX结构中,不好维护
使用bind
使用 bind 指明 this 指向
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick() {
console.log(this.state.msg)
}
render() {
return (
<div>
<button onClick={this.handleClick.bind(this)}>点我</button>
</div>
)
}
}
或者(在构造器中使用bind)
class App extends React.Component {
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
this.msg = '消息';
}
state = {
msg: 'hello react'
}
handleClick() {
console.log(this.state.msg)
console.log(this.msg)
}
render() {
return (
<div>
<button onClick={this.handleClick}>点我</button>
</div>
)
}
}
class实例方法
class App extends React.Component {
state = {
msg: 'hello react'
}
handleClick = () => {
console.log(this.state.msg)
}
render() {
return (
<div>
<button onClick={this.handleClick}>点我</button>
</div>
)
}
}
注意:这个语法是试验性的语法,但是有babel的转义,所以没有任何问题
组件中如何给函数传递参数
箭头函数包裹进行参数传递
推荐
绑定事件函数时传递参数,可以直接绑定一个箭头函数,然后在箭头函数里面执行逻辑函数
import React, { Component } from 'react'
export default class ClickClass extends Component {
constructor() {
super();
this.msg = '消息';
}
click = (id) => {
console.log('类组件点击事件');
console.log(id);
}
render() {
return (
<div>
<div onClick={() => this.click(10)}>类组件点击事件</div>
</div>
)
}
}
获取event:
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor() {
super()
this.state = {
title: 'hello world',
}
}
changeTitle(event, title, title1) {
this.setState({
title: title,
})
console.log(title)
console.log(event)
console.log(title1)
}
render() {
const { title } = this.state
return (
<div>
<p>{title}</p>
<button
onClick={(event) => this.changeTitle(event, '哈哈哈', '嘻嘻嘻')}
>
更改
</button>
</div>
)
}
}
使用.bind
传递参数
import React, { Component } from 'react'
export default class ClickClass extends Component {
constructor() {
super();
this.msg = '消息';
}
click = (id) => {
console.log('类组件点击事件');
console.log(id);
}
render() {
return (
<div>
<div onClick={this.click.bind(this, 10)}>类组件点击事件</div>
</div>
)
}
}
Tab案例
import React, { Component } from 'react'
export default class ClickClass extends Component {
constructor() {
super();
this.state = {
tabs: ['tab1', 'tab2', 'tab3'],
currentIndex: 0
};
}
itemClick = (index) => {
this.setState({
currentIndex: index
})
}
render() {
const {tabs, currentIndex} = this.state;
return (
<div>
<ul>
{
tabs.map((item, index) => {
return (
<li className= {currentIndex === index ? 'active' : ''} key="item" onClick={() => this.itemClick(index)}>{item}</li>
)
})
}
</ul>
</div>
)
}
}
传递多个参数:
import React, { PureComponent } from 'react'
export default class demoClassComponent extends PureComponent {
constructor() {
super()
this.state = {
title: '哈哈哈',
}
}
click = (event, id, id1) => {
console.log(event)
console.log(id)
console.log(id1)
}
render() {
const { title } = this.state
return (
<div>
<div onClick={this.click.bind(this, 10, 20)}>类组件点击事件</div>
</div>
)
}
}
事件机制
React 基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等
在 React 中这套事件机制被称之为合成事件
合成事件(SyntheticEvent)
合成事件是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据 M3C 规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如:
const button = <button onClick={handleClick}></button>
如果想要获得原生 DOM 事件,可以通过 e.nativeEvent 属性获取
const handleClick = (e) => console.log(e.nativeEvent)
const button = <button onClick={handleClick}>按钮</button>
从上面可以看到 React 事件和原生事件也非常的相似,但也有一定的区别:
事件名称命名方式不同
事件处理函数书写不同
虽然 onclick 看似绑定到 DOM 元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听
这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时, 只是在这个统一的事件监听器上插入或删除一些对象
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升
执行顺序
关于 React 合成事件与原生事件执行顺序,可以看看下面一个例子:
结论:
- React 所有事件都挂载在 document 对象上
- 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
- 所以会先执行原生事件,然后处理 React 事件
- 最后真正执行 document 上挂载的事件
对应过程如图所示:
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stoplmmediatePropagation()
- 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免
React 事件机制总结如下
事件绑定的方式
在 react应用中,事件名都是用小驼峰格式进行书写,例如 onclick 要改写成 onCLick
如何绑定
为了解决上面正确输出 this 的问题,常见的绑定方式有如下:
render方法中使用bind
如果使用一个类组件,在其中给某个组件/元素一个:onClick 属性,它现在并会自定绑定其 this 到当前组件,解决这个问题的方法是在事件函数后使用 .bind(this)将 this 绑定到当前组件中
这种方式在组件每次 render 渲染的时候,都会重新进行 bind 的操作,影响性能
render方法中使用箭头函数
通过 ES6 的上下文来将 this 的指向绑定给当前组件,同样再每一次 render 的时候都会生成新的方法,影响性能
constructor中bind
在 constructor 中预先 bind 当前组件,可以避免在 render 操作中重复绑定
定义阶段使用箭头函数绑定
跟上述方式三一样,能够避免在 render 操作中重复绑定,实现也非常的简单,如下:
上述四种方法的方式,区别主要如下:
- 编写方面:方式一、方式二写法简单,方式三的编写过于冗杂
- 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例
- 综合上述,方式四是最优的事件绑定方式