前言
今天同事在开发过程中遇到了个问题,在使用AntD的Form组件时,内置的onFinish方法里面调用了2次setState方法,发现return函数渲染了2次,不过我记得多次调用setState时,会批量合并
,所以就产生了一些疑惑,就上网查了一些资料,学习记录一下。
1、setState的使用
使用过React的应该都知道,在React中,一个组件中要读取当前状态需要访问this.state,但是更新状态
却需要使用this.setState
,不是直接在this.state上修改
。
setState(updater, callback)
这个方法是用来告诉react组件数据有更新,有可能需要重新渲染。
就比如这样:
//读取状态
const count = this.state.count;
//更新状态
this.setState({count: count + 1});
或
this.setState(preState=>({count:preState.count + 1}))
//无意义的修改
this.state.count = count + 1;
2、setState的同步和异步
在印象当中,setState是异步的,毕竟日常开发过程中,发现在使用setState
改变状态之后,立刻通过this.state去拿最新的状态往往是拿不到
的。
当时一步步查看资料发现,setState并不是简单的异步就完事了。
如果想详细看代码流程,可以看一下 博主:虹晨 的这篇博客,这里我就不写源码了。
( https://juejin.cn/post/6844903636749778958 )
(1)合成事件
所谓合成事件
,就是react为了解决跨平台,兼容性等问题,自己封装了一套事件机制,代理了原生事件,想在jsx中比较常见的onClick
,onChange
等,都是合成事件。
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新前的val --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
<button onClick={this.increment}>点击我</button>
</div>
)
}
}
我们发现:
- 在
onClick
合成事件中,val并没有在setState后面立即 + 1,在控制台中打印的仍是更改之前的值 0
结论:
合成事件中,setState是“异步”的
(2)生命周期(钩子函数)
以componentDidMount
为例
class App extends Component {
state = { val: 0 }
componentDidMount() {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的还是更新前的值 --> 0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
我们发现:
- 和合成事件一样,在
生命周期
里的setState,val并没有在setState后面立即 + 1,在控制台中打印的仍是更改之前的值 0
结论:
生命周期中,setState是“异步”的
(3)原生事件
所谓原生事件
是指非react合成事件
,例如原生自带的事件监听 addEventListener
,或者也可以用原生js、jq直接 document.querySelector().onclick
这种绑定事件的形式都属于原生事件。
class App extends Component {
state = { val: 0 }
changeValue = () => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val) // 输出的是更新后的值 --> 1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
我们发现:
- 在
原生事件
中,我们监听click事件后,val在setState后面立即 + 1,在控制台中打印的是更改之后的值 1,2,3
结论:
原生事件中,setState是“同步”的
(4)异步中调用(setTimeout为例)
在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件
中 setTimeout ,可以在钩子函数
中 setTimeout ,也可以在原生事件
setTimeout。
这里我们在三种情况下都使用一下setTimeout,观察其不同的状态
class App extends Component {
state = { val: 0 }
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log('1111',this.state.val) // 输出更新后的值
}, 6000)
document.body.addEventListener('click', this.changeValue, false)
}
handleClick = () => {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log('2222',this.state.val) // 输出更新后的值
}, 1000)
}
changeValue = () => {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log('3333',this.state.val) // 输出的是更新后的值
},2000)
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
<button onClick={this.handleClick}>点击我</button>
</div>
)
}
}
我们发现:
- 不管是在
合成事件
中 setTimeout ,或者在钩子函数
中 setTimeout ,或者在原生事件
的setTimeout,基于event loop的模型下, setTimeout 中里去 setState总能拿到最新的state值
。
结论:
异步中的setState,会同步执行
(5)总结(源码中的try catch)
在相关源码里面,有一个try finally
语法,注意这里不是try catch
呦,说实话这个try finally
这个语法我之前也没怎么用过,不过查阅资料后发现,这个挺好用的,言归正传。
try finally
简单来说就是会先执行try
代码块中的语句,然后再执行finally
中的代码。
-
在
合成事件
和生命周期
中:是属于try
代码块中的逻辑,而try里面有个return
,所以你执行完setState后的state没有立即更新
,console.log还是之前的state状态;这和个时候执行finally
里面的代码,会先更新你的state
,并且渲染到UI上面
。导致setState
表现为异步
。 -
在
原生事件
中:没有被return,所以会直接更新。导致setState
表现为同步
。 -
在
异步
比如setTimeout
中:当在try
里面执行到setTimeout时,把它丢到任务队列,并没有执行
,而是先执行finally
里面代码块,等finally执行完成后,再到任务队列setState
的时候,走的是和原生事件
一样的分支逻辑。导致setState
表现为同步
。
注意:
- setState的
“异步”
并不是
说内部由异步代码实现
,其实本身执行的过程
和代码
都是同步
的,只是
合成事件和钩子函数的调用顺序在更新之前
,导致
在合成事件和钩子函数中没法立马拿到更新后的值
,形式了所谓的“异步”
。
结论:
- setState 只在
合成事件
和钩子函数
中是“异步”
的 - 在
原生事件
和setTimeout
中都是同步
的
3、setState的参数
正常情况下,setState(arg1,arg2)括号内可以传递两个参数
(1)参数一:arg1
参数一arg1可以传两种
形式,第一种是对象
,第二种是函数
(1)对象式:
this.setState({ val : 1});
this.setState({ val : this.state.val + 1});
(2)函数式:
这个函数会接收到两个参数
,第一个是当前的state
,第二个是当前的props
,这个函数应该返回一个对象
,这个对象代表想要对this.state的更改。换句话说,之前你想给this.setState传递什么对象参数,在这种函数里就返回什么对象,不过,计算这个对象的方法有些改变,不再依赖于this.state,而是依赖于输入参数state。
这个函数格式是固定的,必须第一个参数是state的前一个状态,第二个参数是属性对象props,这两个对象setState会自动传递到函数中去
写法一
this.setState((preState, props) => {
return {val: props.val}
});
写法二
this.setState((preState, props) => ({
isShow: !preState.isShow
}));
注意:
- 如果新状态不依赖于原状态--------使用对象方式 ( 对象方式是函数方式的简写方式 )
- 如果新状态依赖于原状态 --------使用函数方式
有时你可以在return之前做些什么,比如
this.setState((preState,props)=>(
console.log('111',this.state.val),
{val:preState.val + 5}
))
//或者
this.setState((preState,props)=>{
console.log('333',this.state.val)
return {
val : preState.val + 10
}
})
不过一般这种场景很少
(2)参数二:arg2
一个回调函数callBack
,当setState结束并重新呈现该组件时将被调用。
如果需要在setState()后获取最新的状态数据, 在第二个callback函数中读取
this.setState({aa:1},()=>{
console.log('是setState更新完,页面render完,再执行这个函数')
})
4、setState的批量更新
React的官方文档中有这么一句话:
状态更新会合并(也就是说多次setstate函数调用产生的效果会合并)
比如下面这种情况:
class Demo extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState({ val: this.state.val + 1 })
console.log('111',this.state.val)
this.setState({ val: this.state.val + 1 })
console.log('222',this.state.val)
this.setState({ val: this.state.val + 1 })
console.log('333',this.state.val)
}
render() {
console.log('444',this.state.val)
return (
<div>
{`Counter is ${this.state.val}`}
<button onClick={this.batchUpdates}>点击我</button>
</div>
)
}
}
可以发现,在函数batchUpdates里面有3次setState,但是我们每次点击的时候,val只是 + 1。
- 在 setState 的时候react内部会创建一个
updateQueue
,通过firstUpdate
、lastUpdate
、lastUpdate.next
去维护一个更新的队列,在最终的performWork
中,相同的key会被覆盖
,只会对最后一次的 setState 进行更新
上面code相当于下面这种:
class Demo extends Component {
state = { val: 0 }
batchUpdates = () => {
const currentCount = this.state.val;
this.setState({val: currentCount + 1});
console.log('111',this.state.val)
this.setState({val: currentCount + 1});
console.log('222',this.state.val)
this.setState({val: currentCount + 1});
console.log('333',this.state.val)
}
render() {
console.log('444',this.state.val)
return (
<div>
{`Counter is ${this.state.val}`}
<button onClick={this.batchUpdates}>点击我</button>
</div>
)
}
}
currentCount
就是一个快照结果
,重复
地给count设置同一个值
,不要说重复3次,哪怕重复一万次,得到的结果也只是增加1而已
。
如果你想得到结果是3,应该怎么做呢?
这是就不需要对象式的参数,可以使用第二种函数式的参数:
class Demo extends Component {
state = { val: 0 }
batchUpdates = () => {
this.setState(prevState => ({
val: prevState.val + 1
}));
console.log('111',this.state.val)
this.setState(prevState => ({
val: prevState.val + 1
}));
console.log('222',this.state.val)
this.setState(prevState => ({
val: prevState.val + 1
}));
console.log('333',this.state.val)
}
render() {
console.log('444',this.state.val)
return (
<div>
{`Counter is ${this.state.val}`}
<button onClick={this.batchUpdates}>点击我</button>
</div>
)
}
}
这样,每一次改变val的时候,都是prevState.val + 1,pervState是前一个状态,每次setState之后,前一个状态都会改变
,那么这时候,结果就是想要的3了。
所以,如果需要立即 setState
,那么传入一个函数式
来执行setState是最好的选择。
5、setState的批量更新实例
注意区分函数式
和对象式
的区别:
(1)对象式
class Example extends React.Component{
state = {
count: 0,
num:100
};
componentDidMount(){
this.setState({count: this.state.count + 1});
console.log('a',this.state.count)
this.setState({count: this.state.count + 7});
console.log('b',this.state.count)
this.setState({count: this.state.count + 4});
console.log('c',this.state.count)
this.setState(preState => {
console.log('1111',preState)
return{
num: preState.num + 1
}
}, () => {
console.log('d' , this.state.count)
})
}
render(){
console.log('render',this.state.count)
return(
<div>
<div>count值:{this.state.count}</div>
<div>num值:{this.state.num}</div>
</div>
)
}
}
(2)函数式
class Example extends React.Component{
state = {
count: 0,
num:100
};
componentDidMount(){
this.setState(preState => ({ count: preState.count + 1 }))
console.log('a',this.state.count)
this.setState(preState => ({ count: preState.count + 7 }))
console.log('b',this.state.count)
this.setState(preState => ({ count: preState.count + 4 }))
console.log('c',this.state.count)
this.setState(preState => {
console.log('1111',preState)
return{
num: preState.num + 1
}
}, () => {
console.log('d' , this.state.count)
})
}
render(){
console.log('render',this.state.count)
return(
<div>
<div>count值:{this.state.count}</div>
<div>num值:{this.state.num}</div>
</div>
)
}
}
(3)对象式和函数式混合
(1)实例一
class Example extends React.Component{
state = {
count: 0,
num:100
};
componentDidMount(){
this.setState({count: this.state.count + 1});
console.log('a',this.state.count)
this.setState({count: this.state.count + 7});
this.setState(preState => ({ count: preState.count + 7 }))
this.setState({count: this.state.count + 7});
console.log('b',this.state.count)
this.setState(preState => ({ count: preState.count + 4 }))
this.setState({count: this.state.count + 6});
console.log('c',this.state.count)
this.setState(preState => {
console.log('1111',preState)
return{
num: preState.num + 1
}
}, () => {
console.log('d' , this.state.count)
})
}
render(){
console.log('render',this.state.count)
return(
<div>
<div>count值:{this.state.count}</div>
<div>num值:{this.state.num}</div>
</div>
)
}
}
(2)实例二
class Example extends React.Component{
state = {
count: 0,
num:100
};
componentDidMount(){
this.setState(preState => ({ count: preState.count + 1 }))
console.log('a',this.state.count)
this.setState({count: this.state.count + 7});
console.log('b',this.state.count)
this.setState(preState => ({ count: preState.count + 4 }))
console.log('c',this.state.count)
this.setState(preState => {
console.log('1111',preState)
return{
num: preState.num + 1
}
}, () => {
console.log('d' , this.state.count)
})
}
render(){
console.log('render',this.state.count)
return(
<div>
<div>count值:{this.state.count}</div>
<div>num值:{this.state.num}</div>
</div>
)
}
}
(3)实例三
class Example extends React.Component{
state = {
count: 0,
num:100
};
componentDidMount(){
this.setState({count: this.state.count + 1});
console.log('1:' + this.state.count) //2==>0
this.setState({count: this.state.count + 1});
console.log('2:' + this.state.count) //3==>0
setTimeout(() => {
this.setState({count: this.state.count + 1});
console.log('3:' + this.state.count) //9==>4
}, 0)
this.setState(preState => ({ count: preState.count + 1 }), () => {
console.log('4:' + this.state.count) //7==>3
})
console.log('5:' + this.state.count) //4==>0
this.setState(preState => ({ count: preState.count + 1 }))
console.log('6:' + this.state.count) //5==>0
}
render(){
console.log('render',this.state.count) //1==>0 6==>3 8==>4
return(
<div>
<div>count值:{this.state.count}</div>
<div>num值:{this.state.num}</div>
</div>
)
}
(4)结论:
通过观察,我们可以发现函数式
和对象式
的setState有着细微的区别:
多个对象式,且属性相同
时,会合并成一次
setstate,只用看最后一个
对象式的setState```多个函数式
,不会合并
成一个setState,必须计算每一个
对象式前面如果有函数式,则函数式setState不生效
函数式前面如果有对象式,则多次对象式合并为一次,只用看最后一次对象式