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