在 React Hook 出现之前,我们使用类组件来存储当前组件的状态,但是我们在更新数据时,有时候会出现一些与预期结果有差异的问题
举个栗子🌰
组件状态更新、页面渲染时机与预期结果可能存在一些出入
import React, { Component } from 'react'
class StateComp extends Component {
state = {
num: 0
}
handlePlus = () => {
this.setState({
num: this.state.num + 1
})
console.log(this.state.num) // 这里还是上一个 num 值
}
render() {
console.log('render')
return (
<div>
<p>{this.state.num}</p>
<p>
<button onClick={this.handlePlus}>Plus</button>
</p>
</div>
)
}
}
export default StateComp
上面的🌰中,我们可以在控制台看到第一次点击 Plus
按钮之后的输出内容是这样的:
0
'render'
输出顺序自然没有问题 (即更新状态,再触发页面渲染),但既然是在 render 之前,那么我们有理由认为打印的数据 num 应该是更新后的值 1
才对啊...
这是因为 setState
对状态的改变可能是异步的,而 render
方法是在数据改变后触发的,并不是调用 setState
方法后就立即触发;若是立即触发的话,那上面的输出顺序就需要颠倒,结果就应该是:
'render'
1
React 出于性能考虑,会把 异步
的多个 setState()
调用合并成一个调用。同步
调用 setState 则不会进行合并 更新
(如后面的第二个和第三个🌰所示)
如果改变状态的代码处于某个 HTML 元素的事件中,则它是异步的,否则是同步的。所以,如果在某个事件中,需要连续调用多次,则需要使用函数的方式获取最新的状态。
使用示例如下:
// 连续加两次,但更新只有一次
// 返回值会混合覆盖掉之前的状态 (num 属性值)
// onClick 事件处理函数中
handlePlus = () => {
this.setState(prevState => ({
num: prevState.num + 1
}), () => {
// 在 render 之后执行,num 为两次 +1 后的结果
console.log('state 更新完成!', this.state.num)
})
this.setState(prevState => ({
num: prevState.num + 1
}))
}
类似这种 DOM 事件异步操作的情况,this.setState
需要使用回调函数来获取更新后的状态值:
-
第一个参数使用回调函数接受两个参数,即:
上次操作后更新后的 state
、当前的 props
-
第二个参数则可以拿到多个
异步 setState
操作后的状态值,执行时机为:所有状态更新完成,并且完成页面渲染之后
再看一个🌰
import React, { Component } from 'react'
class StateComp extends Component {
state = {
num: 0
}
handlePlus = () => {
const timer = setTimeout(() => {
this.setState({
n: this.state.num + 1
});
this.setState({
n: this.state.num + 1
});
this.setState({
n: this.state.num + 1
});
console.log('end num: ', this.state.num); // 3
// clearTimeout(timer);
}, 1000);
console.log('start num: ', this.state.num); // 0
}
render() {
console.log('render')
return (
<div>
<p>{this.state.num}</p>
<p>
<button onClick={this.handlePlus}>Plus</button>
</p>
</div>
)
}
}
export default StateComp
如上示例代码所示,按钮点击触发 1s 后三次连续调用 setState
,因是同步设置,会三次触发 render
,打印内容为:
'start num:' 0
'render'
'render'
'render'
'end num:' 3
还是上面的🌰(稍稍改动)
import React, { Component } from 'react'
class StateComp extends Component {
state = {
num: 0
}
constructor(props) {
super(props)
// setInterval 内部调用 setState 是同步调用的方式
this.timer = setInterval(() => {
this.setState({
num: this.state.num + 1
})
this.setState({
num: this.state.num + 1
})
this.setState({
num: this.state.num + 1
})
clearInterval(this.timer)
}, 1000)
}
render() {
console.log('render')
return (
<div>
{this.state.num}
</div>
)
}
}
export default StateComp
构造函数中起一个定时器,每 1s 后三次调用 setState 更改 state,则 每隔 1s 都会打印三次 'render'
。而且直接使用 this.state.num
的值也是正确的 (即均是更新后的值)
最佳实践
综上所述,我们可以总结一下类组件更新状态的最佳使用方案:
- 编码过程中将所有的 setState 都当作异步操作的处理方式 (虽然本人更倾向于使用 hook)
- 不要信任 setState 调用后的状态
- 若要使用改变后的状态,则应该使用回调函数的方式,(setState 第二个参数的回调函数会在页面渲染之后执行,可以取到正确的 state)
- 若新的状态需要根据之前的状态进行计算,则使用函数的方式改变状态,即 setState 第一个参数使用回调函数的方式