前言
这篇文章主要是为了纪录一些自己对于 setState 的认识的不断深入的过程.
第一阶段 初识 setState
使用过 React 的应该都知道, 在 React 中, 一个组件中要读取当前状态需要访问 this.state, 但是更新状态却需要使用用 this.setState, 不是直接在 this.state 上修改, 就比如这样:
- // 读取状态
- const count = this.state.count;
- // 更新状态
- this.setState({count: count + 1});
- // 无意义的修改
- this.state.count = count + 1;
其实这主要有几点考虑, 首先 this.state 说到底只是一个对象, 单纯的去修改一个对象的值是毫无意义的, 在 React 中只有去驱动 UI 的更新才会有意义, 因此虽然我们可以尝试直接改变 this.state, 但并没有驱动 UI 的重新渲染, 因此这种操作也就毫无意义. 也正是由于这个原因, 我们就需要使用 this.setState 来驱动组件的更新过程.
然后在我刚学习 React 时, 我就看见了这段很经典的代码:
- function incrementMultiple() {
- this.setState({count: this.state.count + 1});
- this.setState({count: this.state.count + 1});
- this.setState({count: this.state.count + 1});
- }
作为一名 JSer, 我看完就毫不犹豫的想到, 这特么不就是 count 的值加 3 么, 但转眼看了下面的答案, 光速打脸, 实际的结果是 state 只增加了 1. 然后我就不由想到当时没怎么看懂的 React 文档中的一些话: 状态更新可能是异步的, 状态更新合并. 恩, 没毛病, 因为异步且会合并, 因此这三条语句合并为一条语句了, 所以就只执行一次. 然后就扭头溜了, 并没有去思考一些深层次的问题.
第二阶段 setState 理解的进阶
但是随着对 React 的理解的逐步加深, 我开始对 setState 有了更加深的理解:
首先我意识到 this.setState 会通过引发一次组件的更新过程来引发重新绘制. 也就是说 setState 的调用会引起 React 的更新生命周期的四个函数的依次调用:
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
我们都知道, 在 React 生命周期函数里, 以 render 函数为界, 无论是挂载过程和更新过程, 在 render 之前的几个生命周期函数, this.state 和 Props 都是不会发生更新的, 直到 render 函数执行完毕后, this.state 才会得到更新.(有一个例外: 当 shouldComponentUpdate 函数返回 false, 这时候更新过程就被中断了, render 函数也不会被调用了, 这时候 React 不会放弃掉对 this.state 的更新的, 所以虽然不调用 render, 依然会更新 this.state.)
React 的官方文档有提到过这么一句话:
状态更新会合并 (也就是说多次 setstate 函数调用产生的效果会合并).
起初我对这句话理解并不是很深刻, 但按照官方文档的代码示例写了这么一段代码:
- function updateName() {
- this.setState({Age: '22'})
- this.setState({Name: 'srtian'})
- }
果然执行结果与以下代码是等价的
- function updateName() {
- this.setState({Age: '22', Name: 'srtian})
- }
于是我将其理解为一个队列, 每个 this.setState() 都会被合并起来, 排成一排, 到最后一次解决. 但对其设计的原因并不理解, 只知道这样有利于性能 (也是在文档上看到的).
直到理解上面 React 生命周期函数的原理后, 我才理解了 setState 关于这个设计的意图.
前面我们提到过, 每一次使用 setState 都会调用一次更新的生命周期, 如果每一次 this.serState() 都调用一次上面那四个生命周期函数, 虽然以上四个函数都是纯函数, 性能浪费上还好, 但 render 函数会将结果拿去做 Virtual DOM 比较和更新 DOM 树, 这个就比较费时间. 因此, 将多个 this.setSate 进行合并, render 函数就能够将合并后的 this.setState() 的结果一次性的与 Virtual DOM 比较然后更新 DOM 树, 这样就能够用有效的提升性能.
除此之外, 我还认为 setState 的设计十分巧妙, 一般来说只在 render 函数后才会进行更新 this.state. 这其实也避免了 React16 的 Fiber 可能会产生的一个问题: 由于 Fiber 下的组件更新是可以中断, 也就是说在一个组件的更新过程中, 可能更新到一半的时候就由于其他原因而中断更新, 回去做更重要的事情了, 在做完更重要的事情后, 再回来更新这个组件, 这会导致前面的那些生命周期函数可能会执行多次. 因此如果在 render 之前 this.setState() 就改变状态的话, 很有可能就会导致组件状态的多次更新, 从而导致组件状态的混乱.
第三阶段 从源码理解 setstate
经历了上面那个阶段, 我算是对 setState 有那么一些理解了, 但还是不能理解很多东西比如: this.setState() 的是怎么合并的? setState() 到底是怎样一种骚操作?... 等等. 然后我又看见了这段经典的代码:
- class Example extends React.Component {
- constructor() {
- super();
- this.state = {
- val: 0
- };
- }
- componentDidMount() {
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 1 次 log
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 2 次 log
- setTimeout(() => {
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 3 次 log
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 4 次 log
- }, 0);
- }
- render() {
- return null;
- }
- };
恩! 按照我多年经验, 这波操作我看不懂!
image
于是硬着头皮打开了 React 源码, 开始一波瞎分析:
首先就是 setState 了, 可以看出它接受两个参数 partialState 和 callback, 其中 partialState 顾名思义就是部分 state, 起这个名字也能就是想表达它的 state 没有改变 (瞎猜的...). 以下是省略了一部分的代码, 只看核心部分.
- ReactComponent.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);
- if (callback) {
- this.updater.enqueueCallback(this, callback, 'setState');
- }
- };
- enqueueSetState: function(publicInstance, partialState) {
- if (__DEV__) {
- ReactInstrumentation.debugTool.onSetState();
- warning(
- partialState != null,
- 'setState(...): You passed an undefined or null state object;' +
- 'instead, use forceUpdate().',
- );
- }
- var internalInstance = getInternalInstanceReadyForUpdate(
- publicInstance,
- 'setState',
- );
- if (!internalInstance) {
- return;
- }
- var queue =
- internalInstance._pendingStateQueue ||
- (internalInstance._pendingStateQueue = []);
- queue.push(partialState);
- enqueueUpdate(internalInstance);
- }
- // 通过 enqueueUpdate 执行 state 更新
- function enqueueUpdate(component) {
- ensureInjected();
- // 这里是个重点, 插眼!!!!!!!!!!!
- if (!batchingStrategy.isBatchingUpdates) {
- batchingStrategy.batchedUpdates(enqueueUpdate, component);
- return;
- }
- dirtyComponents.push(component);
- if (component._updateBatchNumber == null) {
- component._updateBatchNumber = updateBatchNumber + 1;
- }
- }
- // 对_pendingElement, _pendingStateQueue, _pendingForceUpdate 进行判断,_pendingStateQueue 由于会对 state 进行修改, 所以不为空, 然后会调用 updateComponent 方法
- performUpdateIfNecessary: function(transaction) {
- if (this._pendingElement != null) {
- ReactReconciler.receiveComponent(
- this,
- this._pendingElement,
- transaction,
- this._context,
- );
- } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
- this.updateComponent(
- transaction,
- this._currentElement,
- this._currentElement,
- this._context,
- this._context,
- );
- } else {
- this._updateBatchNumber = null;
- }
- },
然后就看了 updateComponent 方法:
- { // 会检测组件中的 state 和 props 是否发生变化, 有变化才会进行更新;
- // 如果 shouldUpdateComponent 函数中返回 false 则不会执行组件的更新
- updateComponent: function (transaction,
- prevParentElement,
- nextParentElement,
- prevUnmaskedContext,
- nextUnmaskedContext,) {
- var inst = this._instance;
- var nextState = this._processPendingState(nextProps, nextContext);
- var shouldUpdate = true;
- if (!this._pendingForceUpdate) {
- if (inst.shouldComponentUpdate) {
- if (__DEV__) {
- shouldUpdate = measureLifeCyclePerf(
- () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
- this._debugID,
- 'shouldComponentUpdate',
- );
- } else {
- shouldUpdate = inst.shouldComponentUpdate(
- nextProps,
- nextState,
- nextContext,
- );
- }
- } else {
- if (this._compositeType === CompositeTypes.PureClass) {
- shouldUpdate =
- !shallowEqual(prevProps, nextProps) ||
- !shallowEqual(inst.state, nextState);
- }
- }
- }
- },
- // 该方法会合并需要更新的 state, 然后加入到更新队列中
- _processPendingState: function (props, context) {
- var inst = this._instance;
- var queue = this._pendingStateQueue;
- var replace = this._pendingReplaceState;
- this._pendingReplaceState = false;
- this._pendingStateQueue = null;
- if (!queue) {
- return inst.state;
- }
- if (replace && queue.length === 1) {
- return queue[0];
- }
- var nextState = Object.assign({}, replace ? queue[0] : inst.state);
- for (var i = replace ? 1 : 0; i <queue.length; i++) {
- var partial = queue[i];
- Object.assign(
- nextState,
- typeof partial === 'function'
- ? partial.call(inst, nextState, props, context)
- : partial,
- );
- }
- return nextState;
- }
- };
发现它会调用 shouldComponentUpdate 和 componentWillUpdate 方法, 看到这不由理解了一个定律: 不要在 shouldComponentUpdate 和 componentWillUpdate 中调用 setState. 如果在这两个生命周期里调用 setState, 会造成造成循环调用. 好了这个我理解了, 有往前找到了这个 --batchedUpdates:
- var ReactDefaultBatchingStrategy = {
- isBatchingUpdates: false,
- batchedUpdates: function(callback, a, b, c, d, e) {
- var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
- ReactDefaultBatchingStrategy.isBatchingUpdates = true;
- // The code is written this way to avoid extra allocations
- if (alreadyBatchingUpdates) {
- return callback(a, b, c, d, e);
- } else {
- return transaction.perform(callback, null, a, b, c, d, e);
- }
- },
看到这我总算理解了, 当我们调用 setState 时, 最终会通过 enqueueUpdate 执行 state 更新, 就像上面那样有两种更新的模式, 一种是批量更新模式, 将组建保存在 dirtyComponents; 另一种非批量模式, 将会遍历 dirtyComponents, 对每一个 dirtyComponents 调用 updateComponent 方法. 就像这张图:
流程图
至于批量与非批量模式, 会通过 ReactDefaultBatchingStrategy 中的 isBatchingUpdates 属性来进行判断. 在非批量模式下, 会立即应用新的 state; 而在批量模式下, 需要更新 state 的组件会被 push 到 dirtyComponents, 再执行更新.
所以我们再看前面的那坨代码:
- class Example extends React.Component {
- constructor() {
- super();
- this.state = {
- val: 0
- };
- }
- componentDidMount() {
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 1 次 log
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 2 次 log
- setTimeout(() => {
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 3 次 log
- this.setState({val: this.state.val + 1});
- console.log(this.state.val); // 第 4 次 log
- }, 0);
- }
- render() {
- return null;
- }
- };
就不难看出它的答案是 0, 0, 3, 4.
总结起来就是这样:
this.setState 首先会把 state 推入 pendingState 队列中
然后将组件标记为 dirty
React 中有事务的概念, 最常见的就是更新事务, 如果不在事务中, 则会开启一次新的更新事务, 更新事务执行的操作就是把组件标记为 dirty.
判断是否处于 batch update
是的话, 保存组建于 dirtyComponent 中, 等事务处理完后再统一更新.
不是的话, 开启一次新的更新事务, 在标记为 dirty 之后, 开始更新组件. 因此当 setState 执行完毕后, 组件就更新完毕了, 所以会造成定时器同步更新的情况.
来源: http://www.jianshu.com/p/b38a7a4eda2b