一、奇异之处
在React中,setState非常奇怪,初学者都会觉得它肯定是异步的,但是在某些场景下它又是同步的,这很让人疑惑。
先上代码
import React from 'react';
export default class StateDemo extends React.Component {
state = {
count: 0,
};
// 异步更新
onChangeAsyncState = () => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
};
onResetState = () => {
this.setState({
count: 0,
});
console.log(this.state.count);
};
// 同步更新
onChangeSyncState = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
}, 0);
};
render() {
return (
<div>
<p>点击下面按钮,变更count</p>
<button onClick={this.onChangeAsyncState}>
异步更新:{this.state.count}
</button>
<button onClick={this.onResetState}>重置</button>
<button onClick={this.onChangeSyncState}>
同步更新:{this.state.count}
</button>
</div>
);
}
}
复制代码
1.1 异步场景
这是点击异步更新按钮时,输出的结果,没有什么意外可言,点击count,然后count变成1,但是console输出的还是0,请看下面结果图
1.2 同步场景
当点击同步更新按钮时,输出的结果确有点出乎意外之外,state直接同步更新了
1.3 合并更新
其实state除了上面两种场景之外,还有合并更新这一操作,比如下面这段代码
// 合并更新
onChangeMergeState = () => {
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log(this.state.count);
};
render() {
console.log('render', this.state.count);
return (
<div>
<p>点击下面按钮,变更count</p>
<button onClick={this.onChangeMergeState}>
合并更新:{this.state.count}
</button>
</div>
);
}
复制代码
当点击合并按钮时,state的值并不是变成2,而是变成了1,虽然做了两次操作,但是做了一次合并,在最后执行任务队列的时候,实际上是只更新了一次state,注意console.log('render', 1)只执行了1次
[
](react-ts-lchssi.stackblitz.io)
二、设计的原则
setState 最核心的作用是更新state,一旦state变更了状态 ,就会触发组件重新渲染,最后更新视图 UI。
选择什么时间更新state,这个在React RFC讨论中也是一个蛮有争议的话题,早在2017年mobxjs的作者Michel Weststrate就起了一个issue:为什么是setState异步的?为此React的核心成员Dan Abramov做了一次解释,还有包括对接下来React的新功能做了一些提前爆料
- ** 保持内部状态一致性**:props更新是异步的,如果state同步更新了,那会引发新的问题,里面也有issue讨论
- 为未来启用并发更新:根据事件的类型,分配不同的优先级(17之前是用ExpirationTime,之后是用Lanes),并发处理,提高渲染的性能。
在2020年5月1日React的官方人员Andrew Clark()提交了一份关于Lanes的PR,里面提到了lane的优先级和lane的任务并发模式,目前React 17已经可以体验。
上面做了演示,还有当初的一些设计原则,那么它具体的代码是怎么样的尼?
三、源码解读
React当初核心原则就是,整个UI都是一个函数,它接受一些状态(state or data),然后返回整个渲染的UI
用数学公式表达就是
用一句话来解释:state负责计算出状态变化(Reconciler),fn负责把状态渲染在ui view中(Renderer)。
所以this.setState会调用Reconciler(diff算法),用于计算state的变化,最后Renderer(渲染阶段)。
关于diff算法不是本篇的重点,重点说说this.setState的更新逻辑。
以下是setState的源码的注解,明确说了,不能保证会立即执行,因为有可能会被合并执行。
/**
* Sets a subset of the state. Always use this to mutate
* state. You should treat `this.state` as immutable.
*
* There is no guarantee that `this.state` will be immediately updated, so
* accessing `this.state` after calling this method may return the old value.
*
* There is no guarantee that calls to `setState` will run synchronously,
* as they may eventually be batched together. You can provide an optional
* callback that will be executed when the call to setState is actually
* completed.
*
* When a function is provided to setState, it will be called at some point in
* the future (not synchronously). It will be called with the up to date
* component arguments (state, props, context). These values can be different
* from this.* because your function may be called after receiveProps but before
* shouldComponentUpdate, and this new state, props, and context will not yet be
* assigned to this.
*
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/
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');
};
复制代码
this.updater这是一个ReactNoopUpdateQueue对象,从字面意义上来说就是一个更新队列。
接着继续往下执行,
github.com/facebook/re…
2107行代码决定了会去做同步执行的操作,那expirationTime什么时候变成Sync尼?
github.com/facebook/re…
上面写的很清楚,在并发模式之外都是同步执行的,那么什么情况下是并发模式尼?
在React源码中标注了,在Legacy模式下都是同步的github.com/facebook/re…
那现在是Legacy吗?在使用 Concurrent 模式这个文档里面也写了,通过这种调用方式的ReactDOM.render(, rootNode),都是Legacy。
那为什么有的代码不是同步更新尼?因为实际上真正在更新代码的过程中,还有一个参数值非常重要,叫做isBatchingUpdates,如果为true那么就进入合并更新,进而把事件放到延迟队列进行异步更新,如果为false则直接进入同步更新。
isBatchingUpdates在React生命周期内、合成事件中它会自动更新为true
github.com/facebook/re…
四、总结
前面回顾了this.setState设计原因,还有代码的执行逻辑,现在总结下,React官方一开始就是把setState设计成了异步模式,但是为了现实中的一些业务,在Legacy模式下,脱离React上下文环境的情况就出现异步模式,但是在concurrent模式下是肯定都是异步的。
下面把之前的案例修改成concurrent模式执行
// render(<App />, document.getElementById('root')); // Legacy 模式
createRoot(document.getElementById('root')).render(<App />); // concurrent模式
复制代码
输出结果已经变成了和之前异步更新的结果一模一样了。
不过concurrent模式在17版本之前,需要加上unstable_才能使用,17之后已经不需要了。
仔细研究源码之后,发现不用concurrent模式也可以把setTimeout中的同步更新变成异步更新,那就是unstable_batchedUpdates函数,这个函数也是显式的把isBatchingUpdates修改成true,从而把事件放到延迟队列进行执行,有兴趣的小伙伴可以自行尝试下。
以上的源码都是基于16.8.6这个版本,实际上在17版本之后,这块的逻辑已经基于最新的Lanes算法来进行更新了,之前是基于ExpirationTime,后面都是基于Lanes来执行,这样可以带来更高效更直观的更新逻辑。
最后
以上代码,已经上传到stackblitz.io 上了,有兴趣的可以直接点击下面链接,自己体会下
stackblitz.com/edit/react-…