React事件处理
背景
1.通过onXxx属性指定事件处理函数(注意大小写)
- React使用的是自定义(合成)事件, 而不是使用的原生DOM事件
比如原生onclick的事件在React中变成了onClick,这么搞是为了更好的兼容性
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
比如,div标签中有input,button等标签,这里面的事件最终都会被委托给最外层的div上
事件委托的原理是事件冒泡,这么做的原因是高效
2.通过event.target得到发生事件的DOM元素对象 ~~~ 不要过度使用ref
受控组件与非受控组件
功能:需求: 定义一个包含表单的组件
输入用户名密码后, 点击登录显示输入的信息
非受控组件(不推荐)
非受控组件主要是用ref来做管理,而ref获取的是DOM,React官方建议少用ref
比如一个表单,点击button进行提交的这个动作,作为H5的默认动作,默认是不阻止提交的,这个就叫非受控组件
<script type="text/babel">
class Login extends React.Component{
handleSubmit=()=>{
//阻止默认的表单提交
event.preventDefault()
// 解构对象,拿到ref设置的DOM
// 其实这是一种现用现取的逻辑,所以是非受控组件
const {username,password} =this
alert(`${username.value},${password.value}`)
}
render(){
return (
<div>
<form action="www.atguigu.com" onSubmit={this.handleSubmit}>
{/*将属性通过ref设置到对象身上,以便使用时获取*/}
用户名<input ref={(c)=>this.username=c} type="text" name="username"></input>
密码<input ref={(c)=>this.password=c} type="password" name="password"></input>
<button>登陆</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("test"))
</script>
受控组件(推荐)
页面中所有输入的DOM(input,radio…等等),输入的时候,就把输入的内容,set到state里面,等需要用的时候,再取出来
而不是像非受控组件一样,等到需要用的时候,再去现获取。而且非受控组件写了大量的ref属性,在React看来也是不推荐的写法(类似vue双向绑定)
例子:
稍加修改,我们把数据暂存到state里面,用state来暂存可以达到同样的效果
如果是多个的属性,用ref获得到大量的DOM元素就会很乱,不如统一扔进state更方便
最后代码:
<script type="text/babel">
class Login extends React.Component{
//初始化一下state,如果不写也可以
state={
username:"",
password:""
}
// 管理表单中的n多个属性
saveUsername=(event)=>{
//set的时候用this.setState({key:value})
this.setState({username:event.target.value})
}
savePassword=(event)=>{
// 注意要用setState的API,如果是this.state就会被认为是更新操作
this.setState({password:event.target.value})
}
showValue=(event)=>{
//阻止提交后发生跳转
event.preventDefault()
// 解构,这时就得到的属性,而不是之前的DOM了
const {username,password}=this.state
//所以就不再需要再像ref一样去DOM.value,因为直接获取到的就是属性,而不是DOM
console.log(username,password)
}
render(){
return (
<div>
<form onSubmit={this.showValue}>
{/*每次change的时候都会实时同步到state里面*/}
用户名<input onChange={this.saveUsername} type="text" name="username"></input>
密码<input onChange={this.savePassword} type="password" name="password"></input>
<button>登陆</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("test"))
</script>
着重注意一下这里,其实就是ref 解构出来DOM和state 解构出来属性上的区别
柯里化概念
只有函数才有柯里化
高阶函数: 如果一个函数符合下面2个规范中的任何一个,那该函数就是高阶函数。
- 若A函数,接收的参数是一个函数,那么A就可以称之为高阶函数。
- 若A函数,调用的返回值依然是一个函数,那么A就可以称之为高阶函数。
常见的高阶函数有: Promise、setTimeout、arr.map()等等
函数的柯里化: 通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
高阶函数——函数柯里化
上面的内容其实不难发现,有很大优化空间
比如:
如果有大量的属性,那么复用性很差,有大量冗余,所以不建议这么搞
// 管理表单中的n多个属性
saveUsername=(event)=>{
//set的时候用this.setState({key:value})
this.setState({username:event.target.value})
}
savePassword=(event)=>{
// 注意要用setState的API,如果是this.state就会被认为是更新操作
this.setState({password:event.target.value})
}
<input onChange={this.saveUsername} type="text" name="username"></input>
理所当然的就想着抽共同调用,抽共通的话就需要把当前的key的名字传进去
重点来了
从这样
<input onChange={this.saveUsername} type="text" name="username"></input>
到这样
<input onChange={this.saveUsername("username")} type="text" name="username"></input>
会有一个问题,就是this.saveUsername
是一个属性this.saveUsername("username")
是一个函数,而这个函数是没有返回值的!
这样就会导致onChange得到的函数return一个undefined。这里的undefined是因为没有标记return。
所以怎么办呢,就是把函数的return用箭头函数改造一下!我让return有返回值,但是返回一个空的箭头处理,即有了返回值,又有了处理。
// 管理表单中的n多个属性
saveValue = (value) => {
//这个event是React自动传入,我们无法手动set,所以不能在方法里传入
return (event) => {
this.setState({ [value]: event.target.value });
};
};
//调用时传入key
<input onChange={this.saveUsername("username")} type="text" name="username"/>
这里为什么value要用[ ]套一下?
this.setState({ [value]: event.target.value });
这里的value用[]套一下是因为它是当前对象的一个变量,而不是一个字符串。在JavaScript中,当你使用方括号([])来访问对象的属性时,如果属性名是一个变量,那么你需要将变量放在方括号内。这样,你就可以动态地访问和设置对象的属性值。
换句话说,这个方括号([]),直接把value从字符串转换成对象的一个属性了
贴上完整代码:
<!--将JSX转换成JS用的依赖,因为浏览器不认识JSX,只认识JS-->
<script type="text/babel">
class Login extends React.Component {
//初始化一下state,如果不写也可以
state = {
username: "",
password: "",
};
// 管理表单中的n多个属性
saveValue = (value) => {
//这个event是React自动传入,我们直接取值即可
return (event) => {
this.setState({ [value]: event.target.value });
};
};
render() {
return (
<div>
<form>
用户名
<input
onChange={this.saveValue("username")}
type="text"
name="username"
></input>
密码
<input
onChange={this.saveValue("password")}
type="password"
name="password"
></input>
<button>登陆</button>
</form>
</div>
);
}
}
ReactDOM.render(<Login />, document.getElementById("test"));
</script>
不用柯里化的写法
不用柯里化对其进行改造,也就是不再采用上面的return箭头函数
关键在于在标签里就把event传进去
改造一下,此时event就直接传入方法了:
<input onChange={(event) => {this.saveValue("username", event)} type="text" name="username" ></input>
saveValue = (value, event) => {
this.setState({ [value]: event.target.value });
};
测试效果是一样的,好用
全部代码:
<script type="text/babel">
class Login extends React.Component {
//初始化一下state,如果不写也可以
state = {
username: "",
password: "",
};
// 管理表单中的n多个属性
saveValue = (value, event) => {
this.setState({ [value]: event.target.value });
};
render() {
return (
<div>
<form>
用户名
<input
onChange={(event) => {
this.saveValue("username", event);
}}
type="text"
name="username"
></input>
密码
<input
onChange={(event) => {
this.saveValue("password", event);
}}
type="password"
name="password"
></input>
<button>登陆</button>
</form>
</div>
);
}
}
ReactDOM.render(<Login />, document.getElementById("test"));
</script>
React生命周期
React生命周期可以分为旧版和新版,这里旧版生命周期并非弃用,新版只是在旧版生命周期的上面,废弃了三个API,新增了两个API,所以还是有学习旧版生命周期的必要性。
旧版生命周期
可以看到,可以分为两大部分,分别为挂载初始化时和组件更新时
render函数贯穿两大部分,因为无论是初始化,还是组件更新,都需要重新渲染,diffing差分等等。
每个部分都有对应的函数供我们使用。
初始化阶段API(由ReactDOM.render()触发—初次渲染)
主要集中于这四个API,无论代码的顺序如何,都会按照这个生命周期的顺序进行执行。
-
1、
constructor()
完成了React数据的初始化,state设值等等。 -
2、
componentWillMount()
组件已经完成初始化数据,但是还未渲染DOM时执行的逻辑,主要用于服务端渲染,网络请求等。 -
3、
render()
组件开始渲染真实DOM。 -
4、
componentDidMount()
组件第一次渲染完成时执行的逻辑,此时DOM节点已经生成了。
代码:
<script type="text/babel">
class Login extends React.Component {
constructor(Props) {
super(Props);
console.log("constructor 执行");
}
componentWillMount() {
console.log("componentWillMount 执行");
}
componentDidMount() {
console.log("componentDidMount 执行");
}
render() {
console.log("render 执行");
return <div>123</div>;
}
}
ReactDOM.render(<Login />, document.getElementById("test"));
</script>
执行可以发现,按照生命周期的顺序输出
更新阶段API(由组件内部this.setSate()或父组件重新render触发)
一般来说,更新阶段API都是在属性变更时触发,比如state属性变化,页面上组件变化,就会触发更新阶段的API
-
1、componentWillReceiveProps(nextProps)
接收父组件新的props时,重新渲染组件执行的逻辑,括号里的参数可以选择接收之后打印一下。注意,这个API在初次接收的时候是不会更新的,只有第二次接收之前才会触发 -
2、shouldComponentUpdate(nextProps, nextState)
在setState以后,state发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中return false可以阻止组件的更新,主要用于性能优化。 -
3、componentWillUpdate(nextProps, nextState)
shouldComponentUpdate返回true以后,组件进入重新渲染的流程时执行的逻辑。 -
4、render()
页面渲染执行的逻辑,render函数把jsx编译为函数并生成虚拟dom,然后通过其diff算法比较更新前后的新旧DOM树,并渲染更改后的节点。 -
5、componentDidUpdate(prevProps, prevState)
重新渲染后执行的逻辑。
父子组件传值例子:
定义了两个类组件(A组件 , B组件),B组件在A组件内使用,A组件向B组件传值,每次只要值更新,就会触发更新生命周期函数
<script type="text/babel">
class A extends React.Component {
// 给state默认值
state = {
car: "宝马",
};
// 更新传入值
changeCar = () => {
this.setState({ car: "奥迪" });
};
render() {
return (
<div>
<div>A组件</div>
<button onClick={this.changeCar}>更新组件,给B传值触发更新</button>
{"B组件传入A的state属性"}
<B car={this.state.car} />
</div>
);
}
}
class B extends React.Component {
render() {
return <div>{this.props.car}</div>;
}
}
ReactDOM.render(<A />, document.getElementById("test"));
引出API
在这个传值的过程中会触发很多生命周期API,挨个试验一下
- 1、componentWillReceiveProps(nextProps)
接收父组件新的props时,重新渲染组件执行的逻辑,括号里的参数可以选择接收之后打印一下。注意,这个API在初次接收的时候是不会更新的,只有第二次接收之前才会触发
- 2、shouldComponentUpdate(nextProps, nextState)
参数不传也完全可以
setState操作就会触发这个钩子,,state发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中return false或不指定返回内容(默认undefined) 可以阻止组件的更新
,相当于更新组件的阀门。
稍微修改一下代码,手动return true,就可以继续更新组件了
shouldComponentUpdate() {
console.log("shouldComponentUpdate 执行了!");
return true;
}
- 3、componentWillUpdate(nextProps, nextState)
参数不传也完全可以
shouldComponentUpdate返回true以后,组件进入重新渲染的流程时执行的逻辑。通俗讲就是渲染前的钩子
- 4、render()
页面渲染执行的逻辑,render函数把jsx编译为函数并生成虚拟dom,然后通过其diff算法比较更新前后的新旧DOM树,并渲染更改后的节点。 - 5、componentDidUpdate(prevProps, prevState)
参数不传也完全可以
重新渲染后执行的逻辑。通俗讲就是渲染完毕后的钩子
代码(这里只有更新阶段钩子)
<script type="text/babel">
class A extends React.Component {
// 给state默认值
state = {
car: "宝马",
};
// 更新传入值
changeCar = () => {
this.setState({ car: "奥迪" });
};
shouldComponentUpdate() {
console.log("A标签的 shouldComponentUpdate 执行了!");
return true;
}
componentWillUpdate() {
console.log("A标签的 componentWillUpdate 执行了!(组件渲染前)");
return false;
}
componentDidUpdate() {
console.log("A标签的 componentDidUpdate 执行了!(组件渲染完毕)");
return false;
}
render() {
console.log("A标签的 render 执行了!");
return (
<div>
<div>A组件{this.state.car}</div>
<button onClick={this.changeCar}>更新A组件</button>
</div>
);
}
}
ReactDOM.render(<A />, document.getElementById("test"));
</script>
组件卸载(由ReactDOM.unmountComponentAtNode()触发)
最后一个API,在组件卸载之前触发
- componentWillUnmount()
组件卸载方法:ReactDOM.unmountComponentAtNode()
组件的卸载前执行的逻辑,比如进行“清除组件中所有的setTimeout、setInterval等计时器”或“移除所有组件中的监听器removeEventListener”等操作。
旧版生命周期总结
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
1. constructor()
2. componentWillMount()
3. render()
4. componentDidMount() =====> 常用
一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件render触发
1. shouldComponentUpdate()
2. componentWillUpdate()
3. render() =====> 必须使用的一个,不渲染没法显示
4. componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
1. componentWillUnmount() =====> 常用
一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
新旧生命周期对比
新版本生命周期相对于旧版本,废弃了三个带will的API(componentWillMount(),componentWillReceiveProps(),componentWillUpdate())
新增了两个API static getDerivedStateFromProps(),getSnapshotBeforeUpdate()
注意:对于已经过时的API不会马上废弃,而是在控制台进行Warning提醒,并且在后续的React版本中,如果非要使用过时的API,就必须在API的前缀上加入UNSAFE_
前缀,作为提醒。
比如,从React17版本开始就得加入前缀:
注意,这里的API UNSAFE并不是说这个API就会有安全问题,而是说在未来的预计更新中有概率出现问题。
比如现在切换到React 17版本,再使用过期API就会有提示
在17版本下,使用过时的componentWillMount
,PS:这里选的是不需要条件就可以触发的API,另外两个过时的API触发都需要条件
componentWillMount() {
console.log("componentWillMount执行");
}
这几个UNSAFE_ 的API本身用的频率就很低,所以了解一下即可
新版生命周期
初始化阶段API
- 1、
constructor()
完成了React数据的初始化,state设值等等。之前写过就不赘述了
- 2、
static getDerivedStateFromProps()
首先这个组件是静态
的,如果不用static,就会报错。看控制台报错,说明这个方法不属于对象,需要用static来进行声明
组件挂载阶段render前的最后一次调用,并且在初始挂载及后续更新时都会被调用。适用于第一次的初始化组件以及后续的更新过程中(包括自身状态更新以及父传子)。返回一个state就更新state,返回一个null就不做更新动作
可以传入props和state
static getDerivedStateFromProps(props,state){
console.log('getDerivedStateFromProps',props,state);
return null
}
如果这个位置返回一个固定的state的对象,那么这个return的值就会一直影响state,导致其无法更新,因为其return的值是一个固定的state。那肯定就更新不了state了。
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps", props, state);
return {"key":"value"};
}
举一个props影响state的例子,用react的官方例子:
- 3.
render()
组件挂载依靠这个api,不render就不会重新渲染,之前说过,这里不赘述了
- 4.
componentDidMount()
组件挂载完毕之后触发的,也就是render之后
//组件挂载完毕的钩子
componentDidMount(){
console.log('Count---componentDidMount');
}
更新阶段API
主要集中在这5个钩子
- 1.
getDerivedStateFromProps()
贯穿于组件挂载阶段和更新阶段的API,React 会在初始挂载和后续更新时调用 render 之前调用它。它应该返回一个对象来更新 state,或者返回 null 就不更新任何内容。
比如我想做一个点击按钮就+1的,更新state就会触发组件更新,就会触发这个API
<script type="text/babel">
//创建组件
class Count extends React.Component{
//构造器
constructor(props){
console.log('Count---constructor');
super(props)
//初始化状态
this.state = {count:0}
}
//加1按钮的回调
add = ()=>{
//获取原状态
const {count} = this.state
//更新状态
this.setState({count:count+1})
}
//若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps
static getDerivedStateFromProps(props,state){
console.log('getDerivedStateFromProps',props,state);
return null
}
render(){
console.log('Count---render');
const {count} = this.state
return(
<div>
<h2>当前求和为:{count}</h2>
<button onClick={this.add}>点我+1</button>
</div>
)
}
}
//渲染组件
ReactDOM.render(<Count count={199}/>,document.getElementById('test'))
</script>
- 2.
shouldComponentUpdate(nextProps, nextState)
和老版本的那个一样,当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。默认返回为true,返回false则阻止更新
控制组件自身或者子组件是否需要更新,尤其在子组件非常多的情况下
在之前的触发state更新的基础上加入API,查看更新时效果
shouldComponentUpdate(props, state) {
console.log("shouldComponentUpdate 执行", props, state);
// return false阻止更新
return false;
}
如果改为return true,那么就会没有问题,继续让state更新
shouldComponentUpdate(props, state) {
console.log("shouldComponentUpdate 执行", props, state);
// return true继续更新
return true;
}
- 3.
render()
这个没啥好说的, 前面的"阀门API"如果调用了,并且都return true了 render就会重新执行,如果不调用那就无所谓了,不会影响render()。
如果反过来, 前面的"阀门API"如果调用了,并且return false了,render后续就无法执行。
- 4.
getSnapshotBeforeUpdate()
在最后一次渲染(提交到dom节点)之前调用,getSnapshotBeforeUpdate(prevProps,prevState,),这两个参数就是DOM更新前的参数。
在更新之前获取快照,返回值传递给componentDidUpdate(prevProps,prevState,?snapshotValue)的snapshotValue上面
。这个"快照"指的是在组件更新之前捕获的某个状态state或数据props的瞬时影像,可以取这个快照里对象的某一个属性,也可以return一个任意的数值。
如果单用这个getSnapshotBeforeUpdate()
,是不行的,要搭配componentDidUpdate()
使用,要接收参数的
如果单用getSnapshotBeforeUpdate()
的话,控制台就会报警
两个搭配在一起使用
//在更新之前,render之后获取快照
getSnapshotBeforeUpdate(prevProps,prevState){
console.log('getSnapshotBeforeUpdate');
return 'atguigu'
}
//组件挂载完毕的钩子
componentDidUpdate(prevProps,precState,snapshot){
console.log('componentDidUpdate 执行!',"getSnapshotBeforeUpdate传入的参数",snapshot);
}
- 5.
componentDidUpdate()
组件更新后触发的函数,和旧版本一样,这里先讨论单独使用componentDidUpdate()
的场景
触发一下组件更新操作,触发API
如果是和getSnapshotBeforeUpdate()
配合使用的话,我们将获取到其返回值
// // 在更新之前,render之后获取快照
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate");
return "atguigu";
}
// //组件挂载完毕的钩子
componentDidUpdate(prevProps, precState, snapshot) {
console.log("componentDidUpdate 执行 组件更新完毕", snapshot);
}
点击更新操作
最后验证一下新版本生命周期钩子的执行顺序
组件卸载(由ReactDOM.unmountComponentAtNode()触发)
- componentWillUnmount()
在组件卸载之前触发
组件卸载方法:ReactDOM.unmountComponentAtNode()
,但是已经在18版本被取代
例子:
<script type="text/babel">
class Count extends React.Component {
componentWillUnmount() {
console.log("componentWillUnmount 执行,组件即将卸载");
}
unMount=()=>{
ReactDOM.unmountComponentAtNode(document.getElementById("test"))
}
render() {
return <div><button onClick={this.unMount}>卸载组件</button></div>;
}
}
//渲染组件
ReactDOM.render(<Count />, document.getElementById("test"));
</script>
单击button,组件就会卸载,在真正卸载之前就会触发componentWillUnmount()
执行
虚拟DOM与DOM Diffing算法
颗粒度
:DOM Diffing算法对比的最小粒度是标签,且逐层对比
- diffing算法应用在 react / vue 中,用于比较虚拟DOM和真实DOM。
- 虚拟DOM中采用的算法,把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作。
- diffing算法会从根节点开始,一层层的向下比较,如果在某一层的某个节点发现不同了,他就会直接替换这个节点下面的所有子树。
- 渲染真实DOM所消耗的性能是很大的,很多操作都会导致页面重新渲染。 通过diff算法对比新旧vdom之间的差异,可以最小化的执行dom操作,从而提高代码性能。
Diffing算法简单解析