一、问题来源
为了学习React的HOOK,我实现了一个简单的计数器,但是把动态修改计数器count的方法通过props传递给子组件时,出现了只生效一次,再次调用不生效的问题。
二、场景介绍
父组件是一个函数式组件,使用了useState来动态更新Count。子组件是类组件,通过父组件传递的回调函数props.changeCount来修改父组件的count。但是出现了调用changeCount不生效的奇怪现象。下面是出现问题的代码,大家可以先分析下问题出在了哪里。
import React,{useState} from 'react';
class FancyButton extends React.Component {
handleClick = this.props.changeCount
render() {
return (
<button onClick={this.handleClick}>
{this.props.label}
</button>
)
}
}
function App() {
const [count, setCount] = useState(0);
const changeCount=()=>{
setCount(count + 1 )
}
return (
<div>
<span>{count}</span>
<FancyButton
label="+"
changeCount={changeCount}
/>
</div>
);
}
export default App;
相信熟悉React函数式组件和类组件工作原理的人一眼就看出来问题所在。
没错是因为我手贱,把this.props.handleClick
赋值给this.handleClick
,才导致点击button
后,计数器无法更新的情况。
为什么出现上述情况呢?
我如果将FancyButton
这个组件做一个简单的改造,可能会更直观一点。
class FancyButton extends React.Component {
constructor(props){
super(props);
//其实这段代码和上面的代码是一样的效果,只不过语法上的改变
//但是这样写就很容易找到问题的所在
this.handleClick = this.props.changeCount;
}
handleClick = this.props.changeCount
render() {
return (
<button onClick={this.handleClick}>
{this.props.label}
</button>
)
}
}
这样简单改造后,我们可能就能之间发现问题所在,下面先介绍为什么第一次点击的时候会成功,然后再说明为什么再次点击的时候会失败。
- 为什么第一次点击的时候会成功?
学习过react声明周期函数的同学肯定知道constructor
函数只会在组件第一次初始化的时候调用一次,所以整个程序初始化的时候我们把父组件的changeCount
函数赋值给FancyButton
的handleClick
进行绑定,所以第一次点击按钮的时候一定会成功。 - 为什么再次点击的时候无效?
现在我们把关注点放在第一次点击后发生了什么react内部发生了什么事情,首先点击后间接触发了setCount()
函数修改了Count
的值,此时App
组件中的state
发生了改变导致重新渲染,其实就是再从头到位执行一遍App
这个函数,那么意味着App
再次执行前后changeCount
函数不是一个函数了,并且每次重新执行count的值仍然是1 由于React性能优化的机制,自组件并不能监测到props.changCount
发生了改变,所以此时组件FancyButton
中的this.handleClick
引用的仍然是第一次constructor初始化式绑定的changeCount
,这样不仅会造成this.handleClick
失去了绑定,而且还造成了内存泄漏😂;
解决方案
- 采用函数式更新state
仍然在构造函数中绑定changeCount
,修改App组件中的changeCount
函数,采用函数式更新state,这样做的原因利用Hook闭包的原理,每次更新调用的setCount
函数中传递的prev更新Count,而这prev是从hook的闭包环境中获取的值,所以每次点击都会改变。function App() { const [count, setCount] = useState(0); const changeCount=()=>{ console.log(count);// 0 //采用函数式更新 setCount(prev=>prev+1) } return ( <div> <span>{count}</span> <FancyButton label="+" changeCount={changeCount} /> </div> ); }
- 在声明周期函数中修改绑定,以
render
为例
在FancyButton
组件中绑定changeCount
,这么做的原因是每次父组件更新会导致更新子组件的render重新执行,在这里做重新绑定也可以达到预期效果class FancyButton extends React.Component { render() { //在此处进行赋值绑定 this.handleClick = this.props.changeCount return ( <button onClick={this.handleClick}> {this.props.label} </button> ) } }
- 惯用的写法(闭包)
这样做是constructor
执行是生成一个箭头函数保存对this.props.changeCount()
这个方法的引用,实质上是利用闭包的原理,所以每次调用相当于在App
调用changeCount()
这函数。
上面这种做法实质上是下面做法闭包的实现,下面是HOOK比较正常的使用方式,参见React HOOK 规则class FancyButton extends React.Component { //这样做实质上是引用了App中的值(函数也是值),产生了闭包 handleClick =()=>this.props.changeCount() render() { return ( <button onClick={this.handleClick} //如果不在上面使用handleClick =()=>this.props.changeCount() //在此直接使用onClick={this.props.changeCount} //原理仍然是闭包,请自行理解 > {this.props.label} </button> ) } } function App() { const [count, setCount] = useState(0); const changeCount=()=>{ //每次打印都是0 console.log(count);// 0 setCount(count + 1) } return ( <div> <span>{count}</span> <FancyButton label="+" changeCount={changeCount} /> </div> ); }
function App() { const [count, setCount] = useState(0); const changeCount=()=>{ //每次结果都是期望的结果0,1,2... //至于为什么,请去阅读一下官方文档HOOK规则,此处不再赘述 console.log(count);// 0->1->2... //注意此处还是赋值式更新 setCount(count + 1) } return ( <div> <span>{count}</span> <button onClick={changeCount} > + </button> </div> ); }
以上就是比较常见的三种解决方案。
未来
如果对此问题仍然存有疑惑,个人觉得应当阅读一下源码,我也会在未来重新用源码实现的角度,重新描述这个问题出现的原因,以及解决的思路。
另外,如果有问题,或者发现文章中的错误欢迎留言指正,谢谢!