前言
今天同事在开发过程中遇到了个问题,在使用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)
}
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)
}
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)
}
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)
this.setState({count: this.state.count + 1});
console.log('2:' + this.state.count)
setTimeout(() => {
this.setState({count: this.state.count + 1});
console.log('3:' + this.state.count)
}, 0)
this.setState(preState => ({ count: preState.count + 1 }), () => {
console.log('4:' + this.state.count)
})
console.log('5:' + this.state.count)
this.setState(preState => ({ count: preState.count + 1 }))
console.log('6:' + 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>
)
}

(4)结论:
通过观察,我们可以发现函数式和对象式的setState有着细微的区别:
多个对象式,且属性相同时,会合并成一次setstate,只用看最后一个对象式的setState```多个函数式,不会合并成一个setState,必须计算每一个对象式前面如果有函数式,则函数式setState不生效函数式前面如果有对象式,则多次对象式合并为一次,只用看最后一次对象式
|