setState是React中使用频率最高的一个API(当然hooks出现之前),它的用法灵活多样,并且也是React面试题经常会考的一个知识点。
在这篇文章中,我对React的setState进行了很多解析,希望可以帮助大家真正理解setState。(其中涉及到一个源码,我有贴出,但是没有详细展开,有机会我们再对源码进行解析,大家不是很懂也不影响你的学习,只需要知道React内部是这样做的即可,面试时也可以回答出来)
一. setState的使用
1.1. 为什么使用setState
回到最早的案例,当点击一个 改变文本
的按钮时,修改界面显示的内容:
![fc5320c7328d40276ba5180f534812c2.png](https://img-blog.csdnimg.cn/img_convert/fc5320c7328d40276ba5180f534812c2.png)
案例的基础代码如下:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: "Hello World"
}
}
render() {
return (
<div><h2>{this.state.message}h2><button onClick={e => this.changeText()}>改变文本button>div>
)
}
changeText() {
}
}
关键是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');
};
![889c12e275b1a6058490744a9ae4cd90.png](https://img-blog.csdnimg.cn/img_convert/889c12e275b1a6058490744a9ae4cd90.png)
所以,我们可以通过调用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);
...
}
![ec60ccced30a856306a0b964f988e116.png](https://img-blog.csdnimg.cn/img_convert/ec60ccced30a856306a0b964f988e116.png)
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;
}