react中setState()是异步的还是同步的,如何控制?
之前一直在做vue开发,可能对vue了解的多一些,但是近期想开始整明白react背后的事情。
仅仅适用于react18版本之前
this.state.count += 1 不会触发数据更新
handleClickDemo = () => {
this.state.count += 1 // 该代码仅在作用域范围之内有效 不会触发页面更新渲染
console.log(this.state.count) // 第1次点击 结果为1 // 第1次点击 结果为3
this.setState({ count: this.state.count + 1 }) // setState才会更新数据 触发页面渲染
this.state.count += 5 // 该行不会改变state中数据
}
setState()的使用说明
setState(updater, [callback])
一、参数1 updater
功能:更新数据,渲染UI
1.1、更新数据
注意:使用该语法时,注意:【后面的setState()不要依赖于前面的setState()更新后的数据】
可能(异步情况下
,什么时候为异步/同步,后面会有介绍)多次调用setState(),但只会触发一次重新渲染
handleClick = () => {
console.log("count", this.state.count) //0
this.setState({ count: this.state.count + 1 })
console.log("count", this.state.count) //0
this.setState({ count: this.state.count + 1 })
console.log("count", this.state.count) //0
this.setState({ count: this.state.count + 1 })
console.log("count", this.state.count) //0
// 页面渲染结果为: 1
}
1.2、推荐语法【如果有两个setState()方法,第二个setState()里面的数据依赖第1个setState()更新后的数据】
推荐:使用setState((state, props) => {})
参数state:表示最新的state
参数props: 表示最新的props
注意:这种语法可能是异步更新
state的,但是都能获取到最新数据
handleClickFn = () => {
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 0 + 1
count: preState.count + 1
}
})
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 1 + 1
count: preState.count + 1
}
})
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 2 + 1
count: preState.count + 1
}
})
console.log("count", this.state.count) //0
// 页面渲染结果为:3
}
二、参数2 callback
场景:在状态更新后并且页面完成重新渲染后
立即执行某个操作
this.setState(
(state, props) => { },
() => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('这里可以打印更改this.state的值', this.state.count)
}
)
handleClick = () => {
console.log("count", this.state.count) //0
this.setState(
{ count: this.state.count + 1 },
() => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('1这里可以打印更改this.state的值', this.state.count) //1
})
console.log("count", this.state.count) //0
this.setState(
{ count: this.state.count + 1 },
() => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('2这里可以打印更改this.state的值', this.state.count) //1
})
console.log("count", this.state.count) //0
this.setState(
{ count: this.state.count + 1 },
() => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('3这里可以打印更改this.state的值', this.state.count) //1
})
console.log("count", this.state.count) //0
// 页面渲染结果为: 1
}
handleClickFn = () => {
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 0 + 1
count: preState.count + 1
}
}, () => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('1这里可以打印更改this.state的值', this.state.count) //3
})
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 1 + 1
count: preState.count + 1
}
}, () => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('2这里可以打印更改this.state的值', this.state.count) //3
})
console.log("count", this.state.count) //0
this.setState((preState, props) => {
return {
// 2 + 1
count: preState.count + 1
}
}, () => {
console.log('这个回调函数会在状态更新后并且页面完成重新渲染后立即执行')
console.log('3这里可以打印更改this.state的值', this.state.count) //3
})
console.log("count", this.state.count) //0
// 页面渲染结果为:3
}
react开发对setState的使用可能一点也不陌生,但肯定会碰到过这种情况
import React, { Component } from 'react'
export default class BatchedDemo extends Component {
state = {
number: 0,
}
handleClick = () => {
this.countNumber()
}
countNumber() {
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 1
})
}
render() {
return <button onClick={this.handleClick}>Num: {this.state.number}</button>
}
}
问题
点击button按钮后,发现只加了1,why?
这就涉及到了setState的更新策略
setState批量更新
除了virtual-dom的优化
,减少数据更新的频率是另外一种手段,也就是React的批量更新。- 顾名思义,批量更新,可以避免短期内的多次渲染,攒为一次性更新。
setState()合并策略:
我对攒为一次性更新的理解是:是覆盖而不是叠加(类似于Object.assign())
证明如下: 把countNumber函数改为如下,如果是覆盖,那么只会执行
number: this.state.number + 5,
相当于把前面同类都覆盖了
countNumber() {
this.setState({
number: this.state.number + 11
})
this.setState({
number: this.state.number + 20
})
this.setState({
number: this.state.number + 5,
})
}
点击按钮后
所以如下操作
this.setState({
age: 18
})
this.setState({
color: 'black‘
})
this.setState({
age: 20
})
this.setState({
name: 'yank'
})
会被React合成为一次setState调用
this.setState({
name: 'yank',
age: 20,
color: 'black'
})
而我们要搞清楚的就是setState()到底是如何去合并的,我能自由控制它的合并吗?
setState()合并原理:
通过伪代码更好的去理解setState()是如何去合并的
setState实现
setState(newState) {
if (this.canMerge) {
this.updateQueue.push(newState)
return
}
// 下面是真正的更新: dom-diff, lifeCycle...
...
}
然后countNumber()方法调用之后,把隐式操作通过伪代码显示出来:
countNumber() {
this.canMerge = true
this.setState({
number: this.state.number + 11
})
this.setState({
number: this.state.number + 20
})
this.setState({
number: this.state.number + 5,
})
this.canMerge = false
// 通过this.updateQueue合并出finalState
const finalState = ...
// 此时canMerge 已经为false 故而走入时机更新逻辑
this.setState(finaleState)
}
可以看出 setState首先会判断是否可以合并,如果可以合并this.canMerge = true
,就直接返回了。直到this.canMerge = false
时,代表finalState已经合并完成,就开始走更新
。
不过有人会问:在使用React
的时候,我并没有设置this.canMerge
呀?我们的确没有,是React
隐式的帮我们设置了!事件处理函数,生命明周期,这些函数的执行是发生在React
内部的,React
对它们有完全的控制权。
canMerge逻辑存在于哪里?
除了事件处理函数会执行canMerge逻辑,在执行componentDidMount前后也会有canMerge逻辑,可以理解为
:React委托代理了所有的事件,在执行你的函数componentDidMount之前,会执行React逻辑,这样React也是有时机执行canMerge逻辑的。
如何控制canMerge逻辑
批量更新是极好滴!我们当然希望任何setState都可以被批量,关键点在于React是否有时机执行canMerge逻辑
,也就是React对目标函数有没有控制权。如果没有控制权,那么就不会执行canMerge逻辑,也就不会发生setState()被react隐式合并了
通过setTimeout脱离react的控制
import React, { Component } from 'react'
export default class BatchedDemo extends Component {
state = {
number: 0,
}
handleClick = () => {
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 2
})
this.setState({
number: this.state.number + 3
})
setTimeout(() => {
this.setState({
number: this.state.number + 4
})
this.setState({
number: this.state.number + 5
})
this.setState({
number: this.state.number + 6
})
})
}
render() {
return <button id="myButton" onClick={this.handleClick}>Num:
{this.state.number}
</button>
}
}
分析上述代码:
handleClick
是事件回调,React
有时机执行canMerge
逻辑,所以x为+1,+2,+3
是合并的,handleClick
结束之后canMerge
被重新设置为false
。注意这里有一个setTimeout(fn, 0)。 这个fn会在handleClick之后调用,而React对setTimeout并没有控制权,React无法在setTimeout前后执行canMerge逻辑
,所以x为4,5,6是无法合并的
,所以fn这里会存在3次dom-diff。React没有控制权的情况有很多: Promise.then(fn), fetch回调,xhr网络回调等等
。
所以点击按钮: 3+4+5+6=18
如何在setTimeout中让react拿回控制权
以上代码的setTimeout中,我想让react去拿回控制权,合并代码,怎么办呢?
需要用unstable_batchedUpdates这个API
代码如下:
import React, { Component } from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
export default class BatchedDemo extends Component {
state = {
number: 0,
}
handleClick = () => {
this.setState({
number: this.state.number + 1
})
this.setState({
number: this.state.number + 2
})
this.setState({
number: this.state.number + 3
})
setTimeout(() => {
//通过这个api,让react拿回控制权,执行canMerge逻辑
batchedUpdates(() => {
this.setState({
number: this.state.number + 4
})
this.setState({
number: this.state.number + 5
})
this.setState({
number: this.state.number + 6
})
})
})
}
render() {
return <button onClick={this.handleClick}>
Num:{this.state.number}
</button>
}
}
打印如下:3+6=9
最后看一下unstable_batchedUpdates这个api的伪代码:
function unstable_batchedUpdates(fn) {
this.canMerge = true
fn()
this.canMerge = false
const finalState = ... //通过this.updateQueue合并出finalState
this.setState(finaleState)
}
总结:
异步的情况:
由React控制
的事件处理函数,以及生命周期函数调用setState时表现为异步 。大部分开发中用到的都是React封装的事件,比如onChange、onClick、onTouchMove等(合成事件中),这些事件处理函数中的setState都是异步处理的。
同步的情况:
React控制之外
的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval,ajax,promise.then内 React 无法掌控的 APIs情况下
,setState是同步更新state的。
setState一定是异步吗?
其实分成两种情况:
- 在组件生命周期或React合成事件中,setState是异步;
- 在setTimeout或者原生dom事件中,setState是同步;
问题回答
通过前面的介绍,我们已经知道:setState什么时候是同步的,什么时候是异步的。
在handleClick方法内调四次setState,为什么 每次点击不是自增4,页面渲染却是1,打印结果却是0呢?
import React, { Component } from 'react'
export default class StateDemo extends Component {
constructor(props) {
super(props)
this.state = { count: 0 }
}
handleClick = () => {
this.setState({ count: this.state.count + 1 })
console.log('第一次handleClick-count', this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log('第二次handleClick-count', this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log('第三次handleClick-count', this.state.count) // 0
this.setState({ count: this.state.count + 1 })
console.log('第四次handleClick-count', this.state.count) // 0
}
render() {
return (
<>
Count: {this.state.count}
<button onClick={this.handleClick}>+</button>
</>
)
}
}
在 React
的 setState
函数实现中,会根据一个变量 isBatchingUpdates
判断是直接更新 this.state
还是放到一个updateQueue
中延时更新,而 isBatchingUpdates
默认是 false
,表示 setState
会同步更新 this.state
;但是,有一个函数 batchedUpdates
,该函数会把 isBatchingUpdates
修改为 true。
因为调用的handleClick事件
属于react的合成事件,更新状态由React控制
,而react内部为了优化setState()的批处理,会对setState()进行合并
,React在调用事件处理函数之前就会先调用这个 batchedUpdates
将isBatchingUpdates
修改为true
,这样由 React
控制事件处理过程的 setState
不会同步更新 this.state
,而是放到一个updateQueue
中延时更新,打印最后一条语句时候还未执行批量更新。所以结果还是打印0,打印语句之后就将isBatchingUpdates
修改为false
,执行updateQueue任务
队列,合并状态:
// var target = { name: 'guxin', age: 18 };
// var source = { state: 'single' }
// Object.assign(target, source, { state: '11' }); //可以看到如果有同名属性的话,后面的属性值会覆盖前面的属性值。
// console.log(target) //{ name: 'guxin', age: 18, state: '11' }
//在React合并多次修改为一次的情况下,相当于等价执行了下面的操作:
Object.assign(
preState,
{count: this.state.count + 1},
{count: this.state.count + 1},
{count: this.state.count + 1},
{count: this.state.count + 1}
)
// 于是,最终只会增加一次操作的值。
this.setState(previousState)
更新状态,触发组件重新渲染,更新视图 UI,所以页面结果为1。
如何让页面渲染结果为4呢?
// 采用这种方式更新数据
setState((state, props) => {})
代码如下
handleClickFn = () => {
this.setState(prevState => {
return { count: prevState.count + 1 }
})
console.log('第一次handleClickFn-count', this.state.count) //0
this.setState(prevState => {
return { count: prevState.count + 1 }
})
console.log('第二次handleClickFn-count', this.state.count) //0
this.setState(prevState => {
return { count: prevState.count + 1 }
})
console.log('第三次handleClickFn-count', this.state.count) //0
this.setState(prevState => {
return { count: prevState.count + 1 }
})
console.log('第四次handleClickFn-count', this.state.count) //0
}
笔试题
componentDidMount() {
// a
this.setState({ count: this.state.count + 1 });
console.log('1:' + this.state.count) //0 ok
// b
this.setState({ count: this.state.count + 1 });
console.log('2:' + this.state.count) //0 ok
setTimeout(() => {
// c
this.setState({ count: this.state.count + 1 });
console.log('3:' + this.state.count) // ? ok
}, 0)
// d
this.setState(preState => ({ count: preState.count + 1 }), () => {
console.log('4:' + this.state.count) // ? ok
})
console.log('5:' + this.state.count) //0 ok
// e
this.setState(preState => ({ count: preState.count + 1 }))
console.log('6:' + this.state.count) // 0 ok
}
打印结果
思考一下,你的答案是什么???
你的答案是否正确?你又是否理解为什么会出现上面的答案?接下来我们就来仔细分析一下。
setState
在上面的代码中,【a,b,c】的 setState 的第一个参数都是一个对象,【d, e】的 setState 的第一个参数都是函数。
1.首先,我们先说说执行顺序的问题。 【1,2,5,6】最先打印,【4】在中间,最后打印【3】。因为【1,2,5,6】是同步任务,【4】是回调,相当于 NextTick 微任务,会在同步任务之后执行,最后的【3】是宏任务,最后执行。
2.接下来说说打印的值的问题。 在【1,2,5,6】下面打印的 state 都是0,说明这里是异步的,没有获取到即时更新的值;
3.在【4】里面为什么打印出3呢?
首先在【a,b】两次 setState 时,都是直接获取的 this.state.count 的值,我们要明白,这里的这个值有异步
的性质,异步就意味着这里不会拿到能即时更新的值,那每次 setState 时,拿到的 this.state.count 都是0。
在【d,e】两个 setState 时,它的参数是函数,这个函数接收的第一个参数 preState (旧的 state ),在这里是同步
的,虽有能拿到即时更新的值,那么经过了【a,b】两次 setState (这里类似于被合并),这里即时的 count 还是1
。因为上面我们说过的执行顺序的关系,再经过【d,e】两次 setState ,所以 count 变成了3。
那么在【3】中打印出4又是为什么?你不是说了在 this.state.count 中拿到的值是“异步”的吗,不是应该拿到0吗,怎么会打印出4呢?
method() {
isBatchingUpdate = true;
// 你需要执行的一些代码
// ...
isBatchingUpdate = false
}
那么在上面的那个面试题中,在 setTimeout 执行的时候 isBatchingUpdate 是 false ,没有命中 batchUpdate 机制,所有同步更新,这里的 this.state.count 已经是 3 了,所有在【3】中打印的就是 4。
componentDidMount(){
isBatchingUpdate = true
setTimeout(() => {
// c
// 由于执行顺序的原因,在这里 isBatchingUpdate 已经是 false 了,所以同步更新
this.setState({ count: this.state.count + 1 });
console.log('3:' + this.state.count)
}, 0)
isBatchingUpdate = false
}