前言
今天同事在开发过程中遇到了个问题,在使用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不生效 函数式前面如果有对象式,则多次对象式合并为一次,只用看最后一次对象式
|