阅读 4201

[React技术内幕] setState的秘密

  对于大多数的React开发者,setState可能是最常用的API之一。React作为View层,通过改变data从而引发UI的更新。React不像Vue这种MVVM库,直接修改data并不能视图的改变,更新状态(state)的过程必须使用setState。   

setState介绍

  setState的函数签名如下:

setState(partialState,callback)复制代码

我们看到setState接受两个参数,一个是partialState,它是新的state用来更新之前的state。callback作为回调函数,会在更新结束之后执行。举个常见的例子

this.setState({
    value: this.state.value + 1
})复制代码

  上面这个例子执行的结果是将state中value的值增加1。但事实真的如此简单吗?我们看下面的代码:   

class Example extends React.Component {
    constructor(props) {
        super(props);
    }

    state = {
        value: 0
    }

    render() {
        return (
            <div>
                <div>The Value: {this.state.value}</div>
                <button onClick={::this._addValue}>add Value</button>
            </div>
        );
    }

    _addValue() {
        this.setState({
            value: this.state.value + 1
        })
        this.setState({
            value: this.state.value + 1
        })
    }
}复制代码

  如果你认为点击"addValue"按妞时每次会增加2的话,说明你可能对setState不是很了解。事实上如果你真的需要每次增加2的话,你的_addValue函数应该这么写:   

_addValue() {
    this.setState((preState,props)=>({
        value: preState.value + 1
    }))

    this.setState((preState,props)=>({
        value: preState.value + 1
    }))
}复制代码

  我们可以看到其实参数partialState不仅可以是一个对象,也可以是一个函数。该函数接受两个参数: 更新前的state(preState)与当前的属性(props),函数返回一个对象用于更新state。为什么会产生这个问题,答案会在后序解答。
  
  其实上面的例子中,如果你真的需要每次增加2的话,你也可以这么写,虽然下面的写法不是很优美:

_addValue() {
    setTimeout(()=>{
        this.setState({
            value: this.state.value + 1
        });
        this.setState({
            value: this.state.value + 1
        });
    },0)
}复制代码

  你现在是否眉头一皱,发现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.

  翻译过来(意译)相当于:

setState用来设置state的子集,永远都只使用setState更改state。你应该将this.state视为不可变数据。

并不能保证this.state会被立即更新,因此在调用这个方法之后访问this.state可能会得到的是之前的值。

不能保证调用setState之后会同步运行,因为它们可能被批量更新,你可以提供可选的回调函数,在setState真正地完成了之后,回调函数将会被执行。

  通篇几个字眼让我们很难办,不保证可能,到底什么时候才会同步更新,什么时候才会异步更新?可能真的需要我们研究一下。   

setState的实现  

  
  React组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法,首先看setState的定义:   

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};复制代码

  我们首先看setState,首先调用的是this.updater.enqueueSetState,先明确this.updater是什么,在React中每个组件有拥有一个this.updater,是用来驱动state更新的工具对象。当我们在组件中的构造函数中调用super时实质调用的就是函数ReactComponent。其中有:   

this.updater = updater || ReactNoopUpdateQueue;复制代码

  没有传入参数updater参数时,this.updater的值就是ReactNoopUpdateQueue。 而ReactNoopUpdateQueue实际是没有什么意义的,只相当于一个初始化的过程。而ReactNoopUpdateQueue.enqueueSetState主要起到一个在非生产版本中警告(warning)的作用。真正的updater是在renderer中注入(inject)的。因此如果你在constructor中尝试调用this.helper.isMounted会返回false,表明组件并没有安装(mount),如果你调用setState,也会给出相应的警告。   

  constructor(props) {
    super(props);
    //这是指个演示,this.isMounted函数已经被废弃
    console.log(this.updater.isMounted())
    this.setState({
        value: 1
    })
}复制代码

  上面的警告就是ReactNoopUpdateQueue中负责打印的。告诉我们在非安装或已卸载的组件上是不能使用setState函数的。
  
  在ReactCompositeComponentMixin中的函数mountComponent中有下面的语句:

inst.updater = ReactUpdateQueue;复制代码

那我们来看看ReactUpdateQueue中的enqueueSetState:


var ReactUpdatedQueue = {
  enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    var queue = internalInstance._pendingStateQueue 
                || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },
}复制代码

我们通过this.updater.enqueueSetState(this, partialState);这里的this是组件的实例,例如在最开始的例子中,this指的就是函数Example的实例(class实质就是函数function的语法糖)。如下图:

通过执行函数

var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');复制代码

  我们得到的internalInstance实质就是组件实例的React内部表达,包含了组件实例的内部的一些属性,例如:
  


  internalInstance的属性很多,但我们需要关注的只有两个:_pendingStateQueue(待更新队列)与_pendingCallbacks(更新回调队列)。根据代码

 var queue = internalInstance._pendingStateQueue 
                || (internalInstance._pendingStateQueue = []);
 queue.push(partialState);复制代码

  如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue。最后执行enqueueUpdate(internalInstance);。因此下一步我们需要研究一下enqueueUpdate

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}复制代码
var ReactUpdates = {
    enqueueUpdate: function enqueueUpdate(component) {
        ensureInjected();
        if (!batchingStrategy.isBatchingUpdates) {
            batchingStrategy.batchedUpdates(enqueueUpdate, component);
            return;
        }

        dirtyComponents.push(component);
    }
}复制代码

  首先执行的ensureInjected()其实也是一个保证ReactUpdates.ReactReconcileTransactionbatchingStrategy是否存在,否则给出相应的警告,当然上面两个的作用之后会给出。接下来会根据batchingStrategy.isBatchingUpdates的值做出不同的行为,如果是true的话,直接将internalInstance放入dirtyComponents,否则将执行batchingStrategy.batchedUpdates(enqueueUpdate, component)。那么我们要了解一下batchingStrategy是干什么的。首先看batchingStrategy的定义:   

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      callback(a, b, c, d, e);
    } else {
      transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};复制代码

  batchingStrategy实质上就是一种批量更新策略,其属性isBatchingUpdates表示的是否处于批量更新的过程中,开始默认值为false。batchedUpdates就是执行批量更新的方法。当isBatchingUpdatesfalse时,执行transaction.perform(callback, null, a, b, c, d, e)。否则当isBatchingUpdatestrue时,直接执行callback。但在我们这里,其实不会执行到这儿,因为当isBatchingUpdatestrue时,直接就将component中放入dirtyComponents中。关于代码中的transaction我们需要了解下React中的事务Transaction。

Transaction

  关于React中的事务Transaction,源码中给出了下面的ASCII图:

/**
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>
 */复制代码

  
  其实上面的形象的解释了React中的事务Transaction,React Transaction会给方法包装一个个wrapper,其中每个wrapper都有两个方法:initializeclose。当执行方法时,需要执行事务的perform方法。perform方法会首先一次执行wrapperinitialize,然后执行函数本身,最后执行wrapperclose方法。
  定义Transaction需要给构造函数混入Transaction.Mixin,并需要提供一个原型方法getTransactionWrappers用于返回wrapper数组。下面我们看下ReactDefaultBatchingStrategy中的transaction是如何定义的:

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

Object.assign(
  ReactDefaultBatchingStrategyTransaction.prototype,
  Transaction.Mixin,
  {
    getTransactionWrappers: function() {
      return TRANSACTION_WRAPPERS;
    },
  }
);

var transaction = new ReactDefaultBatchingStrategyTransaction();复制代码

  其中wrapperRESET_BATCHED_UPDATES负责在close阶段重置ReactDefaultBatchingStrategyisBatchingUpdatesfalse。而wrapperFLUSH_BATCHED_UPDATES负责在close执行flushBatchedUpdates。   

setState更新的过程  

  我们再次回顾一下更新的过程,如果处于批量更新的过程中(即isBatchingUpdates为true),则直接将组件传入dirtyComponents。如果不是的话,开启批量更新,用事务transaction.perform执行enqueueUpdate,这时候isBatchingUpdates经过上次执行,已经是true,将被直接传入dirtyComponents。那么传入更新的组件传入dirtyComponents之后会发生什么?
  
  我们知道,batchedUpdates是处于一个事务中的,该事务在close阶段做了两件事,首先是将ReactDefaultBatchingStrategy.isBatchingUpdates置为false,即关闭批量更新的标志位,第二个就是调用了方法ReactUpdates.flushBatchedUpdatesflushBatchedUpdates中会涉及到Virtual DOM到真实DOM的映射,这不是我们这篇文章的重点(最重要的是我自己也没有参透这边的逻辑),这部分我们只会简要的介绍流程。

//代码有省略
var flushBatchedUpdates = function() {
  while (dirtyComponents.length) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
    //......
  }
};复制代码

  我们发现在函数flushBatchedUpdates中是以事务ReactUpdatesFlushTransaction的方式执行了函数runBatchedUpdates,追根溯源我们来看看runBatchedUpdates干了什么。

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  dirtyComponents.sort(mountOrderComparator);

  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
    //.....
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);
    //.......
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance()
        );
      }
    }
  }
}复制代码

  首先函数将dirtyComponents以组件中的_mountOrder进行了递增排序,其目的就是保证更新顺序,即父组件保证其子组件之前更新。然后在组件中获得setState完成之后的回调函数,开始执行ReactReconciler.performUpdateIfNecessary。又得看看这个函数:

performUpdateIfNecessary: function (internalInstance, transaction) {
    internalInstance.performUpdateIfNecessary(transaction);
}复制代码

  performUpdateIfNecessary执行组件实例的原型方法performUpdateIfNecessary,我们再去看看组件实例是如何定义的这个方法:

var ReactCompositeComponentMixin = {
  performUpdateIfNecessary: function(transaction) {
    //......
    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context
      );
    }
  }
}复制代码

  上面代码是perfromUpdateIfNecessary的省略版本,主要调用的其中的this.updateComponent方法:   

updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext
  ) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    var nextProps;

    // 验证组件context是否改变
    // ......

    // 验证是否是props更新还是组件state更新
    if (prevParentElement === nextParentElement) {
      nextProps = nextParentElement.props;
    } else {
      //存在props的更新  
      nextProps = this._processProps(nextParentElement.props);
      willReceive = true;
    }
    //根据条件判断是否调用钩子函数componentWillReceiveProps
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }
    //计算新的state
    var nextState = this._processPendingState(nextProps, nextContext);

    var shouldUpdate =
      this._pendingForceUpdate ||
      !inst.shouldComponentUpdate ||
      inst.shouldComponentUpdate(nextProps, nextState, nextContext);

    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  }复制代码

  updateComponent方法已经做了相关的注释,其实里面不仅涉及到state的改变导致的重新渲染,还有props的更新导致的重新渲染。在计算新的state时调用了_processPendingState:   

{
  _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;
  }
}复制代码

  这一部分代码相对来说不算是很难,replace是存在是由于之前被废弃的APIthis.replaceState,我们现在不需要关心这一部分,现在我们可以回答刚开始的问题,为什么给setState传入的参数是函数时,就可以解决刚开始的例子。   

Object.assign(
    nextState,
    typeof partial === 'function' ?
        partial.call(inst, nextState, props, context) :
        partial
);复制代码

如果我们传入的是对象

this.setState({value: this.state.value + 1 });
this.setState({value: this.state.value + 1})复制代码

  我们现在已经知道,调用setState是批量更新,那么第一次调用之后,this.state.value的值并没有改变。两次更新的value值其实是一样的,所以达不到我们的目的。但是如果我们传递的是回调函数的形式,那么情况就不一样了,partial.call(inst, nextState, props, context)接受的state都是上一轮更新之后的新值,因此可以达到我们预期的目的。
  
  _processPendingState在计算完新的state之后,会执行_performComponentUpdate:

function _performComponentUpdate(
    nextElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    unmaskedContext
  ) {
    var inst = this._instance;

    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    this._updateRenderedComponent(transaction, unmaskedContext);

    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
}复制代码

  我们可以看到,这部分内容涉及到了几方面内容,首先在更新前调用了钩子函数componentWillUpdate,然后更新了组件的属性(props、state、context),执行函数_updateRenderedComponent(这部分涉及到render函数的调用和相应的DOM更新,我们不做分析),最后再次执行钩子函数componentDidUpdate
  
  到目前为止,我们已经基本介绍完了setState的更新过程,只剩一个部分没有介绍,那就是setState执行结束之后的回调函数。我们知道,setState函数中如果存在callback,则会有:   

  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }复制代码

  call函数会被传递给this.updater的函数enqueueCallback,然后非常类似于setState,callback会存储在组件内部实例中的_pendingCallbacks属性之中。我们知道,回调函数必须要setState真正完成之后才会调用,那么在代码中是怎么实现的。大家还记得在函数flushBatchedUpdates中有一个事务ReactUpdatesFlushTransaction:   

//代码有省略
var flushBatchedUpdates = function() {
  while (dirtyComponents.length) {
    if (dirtyComponents.length) {
      //从事务pool中获得事务实例
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      //释放实例
      ReactUpdatesFlushTransaction.release(transaction);
    }
    //......
  }
};复制代码

  我们现在看看ReactUpdatesFlushTransaction的wrapper是怎么定义的:

var UPDATE_QUEUEING = {
  initialize: function() {
    this.callbackQueue.reset();
  },
  close: function() {
    this.callbackQueue.notifyAll();
  },
};复制代码

  我们看到在事务的close阶段定义了this.callbackQueue.notifyAll(),即执行了回调函数,通过这种方法就能保证回调函数一定是在setState真正完成之后才执行的。到此为止我们基本已经解释了setState大致的流程是怎样的,但是我们还是没有回答之前的一个问题,为什么下面的两种代码会产生不同的情况:   

//未按预期执行
_addValue() {
    this.setState({
        value: this.state.value + 1
    })
    this.setState({
        value: this.state.value + 1
    })
}
//按预期执行
_addValue() {
    setTimeout(()=>{
        this.setState({
            value: this.state.value + 1
        });
        this.setState({
            value: this.state.value + 1
        });
    },0)
}复制代码

  这个问题,其实真的要追本溯源地去讲,是比较复杂的,我们简要介绍一下。在第一种情况下,如果打断点追踪你会发现,在第一次执行setState前,已经触发了一个 batchedUpdates,等到执行setState时已经处于一个较大的事务,因此两个setState都是会被批量更新的(相当于异步更新的过程,thi.state.value值并没有立即改变),执行setState只不过是将两者的partialState传入dirtyComponents,最后再通过事务的close阶段的flushBatchedUpdates方法去执行重新渲染。但是通过setTimeout函数的包装,两次setState都会在click触发的批量更新batchedUpdates结束之后执行,这两次setState会触发两次批量更新batchedUpdates,当然也会执行两个事务以及函数flushBatchedUpdates,这就相当于一个同步更新的过程,自然可以达到我们的目的,这也就解释了为什么React文档中既没有说setState是同步更新或者是异步更新,只是模糊地说到,setState并不保证同步更新。
  
  这篇文章对setState的介绍也是比较浅显的,但是希望能起到一个抛砖迎玉的作用。setState之所以需要会采用一个批量更新的策略,其目的也是为了优化更新性能。但对于平常的使用中,虽然我们不会关心或者涉及到这个问题,但是我们仍然可以使用React开发出高性能的应用,我想这也就是我们喜欢React的原因:简单、高效!

关注下面的标签,发现更多相似文章
评论

查看更多 >