React系列八 – 深入理解setState
<
div>
setState是React中使用频率最高的一个API(当然hooks出现之前),它的用法灵活多样,并且也是React面试题经常会考的一个知识点。
在这篇文章中,我对React的setState进行了很多解析,希望可以帮助大家真正理解setState。(其中涉及到一个源码,我有贴出,但是没有详细展开,有机会我们再对源码进行解析,大家不是很懂也不影响你的学习,只需要知道React内部是这样做的即可,面试时也可以回答出来)
一. setState的使用
1.1. 为什么使用setState
回到最早的案例,当点击一个 改变文本
的按钮时,修改界面显示的内容:

案例的基础代码如下:
import React, { Component } from 'react' export default class App extends Component { constructor(props) { super(props); this.state = { message: "Hello World" } } render() { return () } changeText() { } }{this.state.message}
关键是changeText中应该如何实现:
我们是否可以通过直接修改state中的message来修改界面呢?
- 点击不会有任何反应,为什么呢?
-
因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
-
React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
-
我们必须通过setState来告知React数据已经发生了变化;
changeText() { this.state.message = "你好啊,李银河"; }
我们必须通过setState来更新数据:
- 疑惑:在组件中并没有实现setState的方法,为什么可以调用呢?
-
原因很简单,setState方法是从Component中继承过来的。
Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); };
所以,我们可以通过调用setState来修改数据:
- 当我们调用setState时,会重新执行render函数,根据最新的State来创建ReactElement对象;
-
再根据最新的ReactElement对象,对DOM进行修改;
changeText() { this.setState({ message: "你好啊,李银河" }) }
1.2. setState异步更新
我们来看下面的代码:
- 最终打印结果是Hello World;
-
可见setState是异步的操作,我们并不能在执行完setState之后立马拿到最新的state的结果
changeText() { this.setState({ message: "你好啊,李银河" }) console.log(this.state.message); // Hello World }
为什么setState设计为异步呢?
- setState设计为异步其实之前在GitHub上也有很多的讨论;
-
React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下;
-
https://github.com/facebook/react/issues/11527#issuecomment-360199710;
我对其回答做一个简单的总结:
-
setState
设计为异步,可以显著的提升性能; - 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
-
最好的办法应该是获取到多个更新,之后进行批量更新;
-
如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
-
state和props不能保持一致性,会在开发中产生很多的问题;
那么如何可以获取到更新后的值呢?
- setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
-
格式如下:
setState(partialState, callback)
changeText() { this.setState({ message: "你好啊,李银河" }, () => { console.log(this.state.message); // 你好啊,李银河 }); }
当然,我们也可以在生命周期函数:
componentDidUpdate(prevProps, provState, snapshot) { console.log(this.state.message); }
1.3. setState一定是异步?
疑惑:setState一定是异步更新的吗?
验证一:在setTimeout中的更新:
changeText() { setTimeout(() => { this.setState({ message: "你好啊,李银河" }); console.log(this.state.message); // 你好啊,李银河 }, 0); }
验证二:原生DOM事件:
componentDidMount() { const btnEl = document.getElementById("btn"); btnEl.addEventListener('click', () => { this.setState({ message: "你好啊,李银河" }); console.log(this.state.message); // 你好啊,李银河 }) }
其实分成两种情况:
- 在组件生命周期或React合成事件中,setState是异步;
-
在setTimeout或者原生dom事件中,setState是同步;
React中其实是通过一个函数来确定的:enqueueSetState部分实现(react-reconciler/ReactFiberClassComponent.js)
enqueueSetState(inst, payload, callback) { const fiber = getInstance(inst); // 会根据React上下文计算一个当前时间 const currentTime = requestCurrentTimeForUpdate(); const suspenseConfig = requestCurrentSuspenseConfig(); // 这个函数会返回当前是同步还是异步更新(准确的说是优先级) const expirationTime = computeExpirationForFiber( currentTime, fiber, suspenseConfig, ); const update = createUpdate(expirationTime, suspenseConfig); ... }

computeExpirationForFiber函数的部分实现:
- Sync是优先级最高的,即创建就更新;
currentTime: ExpirationTime, fiber: Fiber, suspenseConfig: null | SuspenseConfig, ): ExpirationTime { const mode = fiber.mode; if ((mode & BlockingMode) === NoMode) { return Sync; } const priorityLevel = getCurrentPriorityLevel(); if ((mode & ConcurrentMode) === NoMode) { return priorityLevel === ImmediatePriority ? Sync : Batched; }
1.4. setState的合并
1.4.1. 数据的合并
假如我们有这样的数据:
this.state = { name: "coderwhy", message: "Hello World" }
我们需要更新message:
- 我通过setState去修改message,是不会对name产生影响的;
changeText() { this.setState({ message: "你好啊,李银河" }); }
为什么不会产生影响呢?源码中其实是有对 原对象
和 新对象进行合并的:
-
事实上就是使用
Object.assign(target, ...sources)
来完成的;

1.4.2. 多个setState合并
比如我们还是有一个counter属性,记录当前的数字:
- 如果进行如下操作,那么counter会变成几呢?答案是1;
-
为什么呢?因为它会对多个state进行合并;
increment() { this.setState({ counter: this.state.counter + 1 }); this.setState({ counter: this.state.counter + 1 }); this.setState({ counter: this.state.counter + 1 }); }
其实在源码的processUpdateQueue中有一个do…while循环,就是从队列中取出多个state进行合并的;

如何可以做到,让counter最终变成3呢?
increment() { this.setState((state, props) => { return { counter: state.counter + 1 } }) this.setState((state, props) => { return { counter: state.counter + 1 } }) this.setState((state, props) => { return { counter: state.counter + 1 } }) }
为什么传入一个函数就可以变出3呢?
- 原因是多个state进行合并时,每次遍历,都会执行一次函数:
二. setState性能优化
2.1. React更新机制
我们在前面已经学习React的渲染流程:

那么React的更新流程呢?

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量;
-
https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
-
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
-
这个开销太过昂贵了,React的更新性能会变得非常低效;
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
- 同层节点之间相互比较,不会垮节点比较;
-
不同类型的节点,产生不同的树结构;
-
开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
2.2. Diffing算法
2.2.1. 对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树: