浅谈setState
经常在项目的数据流乱作一团的时候,层层排查到最后,始作俑者也往往是 setState——工作机制太复杂,前期官方文档不太容易理解。于是结合诸位大佬的文章,做了一下个人总结。
在官方文档 React.Component章节中的描述:
不同于生命周期方法(React 主动调用),以下方法是你可以在组件中调用的方法。
只有两个方法:setState() 和 forceUpdate()。
关于setState
state 到底是同步还是异步的?
想必各位看官都有自己的理解, 在正式切入之前,我们先来看下面这段代码,这是一道变体繁多的面试题,在面试中考察频率非常高。
import React from "react";
import "./styles.css";
export default class App extends React.Component{
state = {
count: 0
}
//点击+1
add = () => {
console.log('add setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
console.log('add setState后的count', this.state.count)
}
//点击+2
addDouble = () => {
console.log('addDouble setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
console.log('addDouble setState后的count', this.state.count)
}
reduce = () => {
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
},0);
}
render(){
return <div>
<button onClick={this.add}>点我增加</button>
<button onClick={this.addDouble}>点我增加2</button>
<button onClick={this.reduce}>点我减少</button>
</div>
}
}
此时有个问题,若从左到右依次点击每个按钮,控制台的输出会是什么样的?读到这里,建议你先暂停 1 分钟在脑子里跑一下代码,看看和下图实际运行出来的结果是否有出入
控制台打印:add setState前的count 0
add setState后的count 0
addDouble setState前的count 1
addDouble setState后的count 1
reduce setState前的count 2
reduce setState后的count 1
在初学的时候会听到过一个结论:setState 是一个异步的方法,这意味着当我们执行完 setState 后,state 本身并不会立刻发生改变。 因此紧跟在 setState 后面输出的 state 值,仍然会维持在它的初始状态(0)。在同步代码执行完毕后的某个时刻,state 才会增加到 1。而我们看到reduce方法中的减法却是同步打印出来了。
解读setState工作流
在上面的小🌰中,又引生出两个疑问:为什么前两个方法是异步的,而加了setTimeout又变成“同步”的了。在此先引用修言大佬的结论:这里我先给出一个结论:并不是setTimeout改变了setState,而是 setTimeout 帮助 setState “逃脱”了 React 对它的管控。只要是在 React 管控下的 setState,一定是异步的。
在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick 和浏览器里的 Event-Loop:每来一个 setState,就把它塞进一个队列里“攒起来”。等时机成熟,再把“攒起来”的 state 结果做合并,最后只针对最新的 state 值走一次更新流程。这个过程,叫作“批量更新”。
this.setState({
count: this.state.count + 1 ===> 入队,[count+1的任务]
});
this.setState({
count: this.state.count + 1 ===> 入队, [count+1的任务,count+1的任务]
});
↓
合并 state,[count+1的任务]
↓
执行 count+1的任务
这就不难理解为什么add和addDouble输出的值了,我们再回到为什么会有“同步”这个现象的产生。我们先来了解一下:setState底层执行过程:调用setState->计算优先级expirationTime->更新调度,调和fiber->合并state,执行render->commit更新真是DOM->执行回调函数。
而在官方文档的解释中:将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。在罕见的情况下,你需要强制 DOM 更新同步应用,你可以使用 flushSync 来包装它,但这可能会损害性能。React-dom提供了flushSync ,flushSync 可以将回调函数中的更新任务,放在一个较高的优先级中。React 设定了很多不同优先级的更新任务。如果一次更新任务在 flushSync 回调函数内部,那么将获得一个较高优先级的更新。
flushSync 在同步条件下,会合并之前的 setState | useState,可以理解成,如果发现了 flushSync ,就会先执行更新,如果之前有未更新的 setState | useState ,就会一起合并了。
关于setState工作流我们先引入这个流程图
batchingStrategy 正是 React 内部专门用于管控批量更新的对象。