本文已参与「新人创作礼」活动,一起开启掘金创作之路。 详情
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 值会被后面所覆盖,最终只会执行一次更新。
this.setState((state, props) => newState)
所谓异步?
setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数
setState(partialState, callback)
中的
callback
拿到更新后的结果
批量更新:
setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和
setTimeout
中不会批量更新。①
在“异步”中如果对同一个值进行多次
setState
,
setState
的批量更新策略会对其进行覆盖,
取最后一次的执行
,② 如果是同时
setState
多个不同的值,在更新时会对其进行合并批量更新。
为什么要setState"异步化"批量处理呢?
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
所有队列中的更新
为什么直接修改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
不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理
Fairy_妍
25.2k