setState是同步还是异步?原理是什么?
setState异步更新
我们都知道,React
通过this.state
来访问state
,通过this.setState()
方法来更新state
。当this.setState()
方法被调用的时候,React
会重新调用render
方法来重新渲染UI
那么setState任何时候都是异步的吗?
首先如果直接在setState
后面获取state
的值是获取不到的。在React
内部机制能检测到的地方, setState
就是异步的;在React检测不到的地方,例如 原生事件addEventListener
,setInterval
,setTimeout
,setState
就是同步更新的
✔setState是同步还是异步呢?
setState并不是单纯的异步或同步,这其实与调用时的环境相关
- 在合成事件 和 生命周期钩子(除
componentDidUpdate
) 中,setState
是"异步"的; - 在 原生事件 和
setTimeout
中,setState
是同步的,可以马上获取更新后的值; - 批量更新:多个顺序的
setState
不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行。在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新。 - 函数式: setState第一个参数为函数形式时,在这个函数中可以回调拿到最新的state对象,然后函数return出的对象讲被设置成newState。
this.setState((state, props) => newState)
所谓异步?
setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的结果
批量更新:
setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout
中不会批量更新。① 在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,② 如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。
为什么要setState"异步化"批量处理呢?
- 做成异步设计是为了性能优化,减少渲染次数
- 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是
- 启用并发更新,完成异步渲染。
setState原理
setState 并非真异步,只是看上去像异步。在源码中,通过
isBatchingUpdates
来判断
setState调用流程:
①调用this.setState(newState)
-> ②将新状态newState存入pending队列 -> ③判断是否处于batch Update
(isBatchingUpdates
是否为true) -> ④isBatchingUpdates
=true,保存组件于dirtyComponents
中,走异步更新流程,合并操作,延迟更新;
⑤isBatchingUpdates
=false,走同步过程。遍历所有的dirtyComponents
,调用updateComponent
,更新pending state or props
setState批量更新的过程
在react
生命周期和合成事件执行前后都有相应的钩子,分别是pre
钩子和post
钩子
-
pre
钩子会调用batchedUpdate
方法将isBatchingUpdates
变量置为true
,也就是将状态标记为现在正处于更新阶段了。开启批量更新。setState
的更新会被存入队列中,待同步代码执行完后,再执行队列中的state
更新。isBatchingUpdates
若为true
,则把当前组件(即调用了setState
的组件)放入dirtyComponents
数组中;否则batchUpdate
所有队列中的更新
- 而
post
钩子会将isBatchingUpdates
置为false
为什么直接修改this.state无效
setState
本质是通过一个队列机制实现state
更新的。 执行setState
时,会将需要更新的state合并后放入状态队列,而不会立刻更新state
,队列机制可以批量更新state
。- 如果不通过
setState
而直接修改this.state
,那么这个state
不会放入状态队列中,下次调用setState
时对状态队列进行合并时,会忽略之前直接被修改的state
,这样我们就无法合并了,而且实际也没有把你想要的state
更新上去
setState之后发生的事情
在setState
调用后,React会去diff state
,若state变化然后会去diff DOM
判断是否更新UI。如果每次setState都去走这些流程可能就会有性能问题。
所有短时间内多次的setState
时,React会将state的改变压入栈中,在合适的时机,批量更新state
和视图,达到提高性能的效果。
setState循环调用风险
- 当调用
setState
时,实际上会执行enqueueSetState
方法,并对partialState
以及_pending-StateQueue
更新队列进行合并操作,最终通过enqueueUpdate
执行state
更新 - 而
performUpdateIfNecessary
方法会获取_pendingElement
,_pendingStateQueue
,_pending-ForceUpdate
,并调用receiveComponent
和updateComponent
方法进行组件更新 - 如果在
shouldComponentUpdate
或者componentWillUpdate
方法中调用setState
,此时this._pending-StateQueue != null
,就会造成循环调用,使得浏览器内存占满后崩溃
判断state输出
看第一个例子:setState的同步异步:
class Test extends React.Component {
state = {
val: 0
};
componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); // 0
setTimeout(() => {
this.setState({ val: this.state.val + 1 });
console.log("setTimeout: " + this.state.val); // 2
}, 0);
}
render() {
return null;
}
};
输出结果0 2
。过程解析:
① 直接在componentDidMount
生命周期中的setState是异步的,此时的val+1并不会立即生效。所以下面第一个的log输出不会拿到最新的值,还是拿到的之前的值,输出0
② 在setTimeout
中的setState是同步的,此时的val可以拿到最新的值,也就是①中最新的val值为1,此时再调用setState
同步给val+1,同步得到val值为2,所以第二次的log输出就是2
再看一个例子:异步中setState的批量更新:
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 输出0
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 输出0
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 输出2
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 输出3
}, 0);
}
输出结果:0 0 2 3
。过程解析:
①前两次的输出都是直接在生命周期
中的输出,所以是异步过程,拿不到最新的值,结果输出都是0;
②前两次的setState
都是设置的val
这同一个key值,更新操作会被覆盖,只执行最后一次,所以相当于只执行了一次val+1
③在setTimeout
中时已经取到了最新的val值为1,此时调用setState
为同步了,执行完setTimeout中的第一个val+1后val=2,这时候的第三个log输出即为2。
④setTimeout
中的第二次setState
同理,为同步过程,直接在val+1=3后的log输出就直接是最新值3
再看关于setState的第二个参数取值:
componentDidMount() {
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count) // 1
})
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count) // 1
})
}
输出结果是1 1
,你猜对了吗?看一下过程解析:
① 在生命周期中的setState
是异步的,此时设置同一个state的key值,操作会被覆盖,相当于只执行了一次count+1,所以实际上的count的值为1
② 我们知道setState
的第二个参数是在更新完成后的回调,可以拿到最新的state。
③ 但是setState时这些回调都是会先把操作函数注入到队列中,等state的批量更新完成后再挨个执行这些回调。实际上执行这两个回调的时机是批量更新后依次执行的,此时的count是1,所以两个都是输出1。
④ 所以可以理解为两次的setState是合并在一起覆盖后只剩下一个。它们各自的回调是合并在一起执行的,所以都输出1
如果我们使用preState
:
componentDidMount() {
this.setState(
preState => ({
count: preState.count + 1
}), () => {
console.log(this.state.count) // 2
})
this.setState(
preState => ({
count: preState.count + 1
}), () => {
console.log(this.state.count) // 2
})
}
输出结果为2 2
。我们知道setState
的第一个参数可以直接是一个对象表示newState,也可以是一个回调函数,拿到上一次的state然后经过操作再return一个newState对象。所以这个执行流程就是:
① 第一个setState时,拿到上一次的count=0后执行+1操作,count变成了1
②第二个setState时,回调参数的preState中的count就是1,此时的+1操作就变成了2
③ 两次的setState的第二个回调参数同时依次执行,输出结果都是最新的count为2
其原因还是由于批量更新。① 如果setState第一个参数是对象,就存储其合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,多次修改同个key值的结果是最终只会执行一次更新;②当第一个参数是函数时,上面两个setState的操作会存两个函数在队列中。会执行了第一个函数后改变合并状态(Object.assign)中的这个key的值(count为1),然后再执行第二个函数时从最新状态获取后再count+1即为2,return出的对象再set合并状态(Object.assign)中的key值count的值为2了。
异步过程的总结
- 通过
setState
去更新this.state
,不要直接操作this.state
,请把它当成不可变的 - 调用
setState
更新this.state
不是马上生效的,它是异步的,所以不要天真以为执行完setState
后this.state
就是最新的值了 - 多个顺序执行的
setState
不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理
转载自:https://juejin.cn/post/7066423854259765279