【React进阶系列】 setState机制

timg (4).jpg

api解析: setState(updater, [callback])


updater: 更新数据 FUNCTION/OBJECT
callback: 更新成功后的回调 FUNCTION
// updater - Function
this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

// update - Object
this.setState({quantity: 2})

setState的特点:


1.异步:react通常会集齐一批需要更新的组件,然后一次性更新来保证渲染的性能
2.浅合并 Objecr.assign()

setState问题与解决

举个


  • 在使用setState改变状态之后,立刻通过this.state去拿最新的状态
    解决: componentDidUpdate或者setState的回调函数里获取
// setState回调函数
changeTitle: function (event) {
  this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
  // Call API with the updated value
}
  • 有一个需求,需要在在onClick里累加两次,使用对象的方法更新,则只会加一次
    解决: 使用updater function
onClick = () => {
    this.setState({ index: this.state.index + 1 });
    this.setState({ index: this.state.index + 1 });
}

// 最后解析为,后面的数据会覆盖前面的更改,所以最终只加了一次.
Object.assign(
  previousState,
  {index: state.index+ 1},
  {index: state.index+ 1},
)

//正确写法
onClick = () => {
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
}

注意:

1.不要在render()函数里面写setstate(),除非你自己定制了shouldComponentUpdate方法,要不然会引起无限循环

render() {
    //this.setState
    return(
        //...dom
    )
}

2.不要给this.state直接复制
react为了实现高效render, state其实是一个队列,setState是将数据插入队列中,使用方式1直接赋值不会触发渲染, react提供了setState的实例方法可以触发render。

// 1
this.state.num = 1
// 2
this.setState({
    num: this.state.num + 1
})

3.对数组和对象等引用对象操作时,使用返回新对象的方法
array: 不要使用push、pop、shift、unshift、splice可使用concat、slice、filter、扩展语法
object: Object.assgin/扩展语法

setState更新机制


如图:


3114633915-5c7108d209e99_articlex.png

图不清楚可以点击查看原图

  • partialStatesetState传入的第一个参数,对象或函数
  • _pendingStateQueue:当前组件等待执行更新的state队列
  • isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
  • dirtyComponent:当前所有处于待更新状态的组件队列
  • transcation:react的事务机制,在被事务调用的方法外包装n个waper对象,并一次执行:waper.init、被调用方法、waper.close
  • FLUSH_BATCHED_UPDATES:用于执行更新的waper,只有一个close方法

2.执行过程

对照上面流程图的文字说明,大概可分为以下几步:

  • 1.将setState传入的partialState参数存储在当前组件实例的_pendingStateQueue中。
  • 2.判断当前React是否处于批量更新状态,如果是,将当前组件标记为dirtyCompontent,并加入待更新的组件队列中。
  • 3.如果未处于批量更新状态,将isBatchingUpdates设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
  • 4.调用事务的waper方法,遍历待更新组件队列依次执行更新。
  • 5.执行生命周期componentWillReceiveProps
  • 6.将组件的state暂存队列中的state进行合并,获得最终要更新的state对象,并将_pendingStateQueue置为空。
  • 7.执行生命周期shouldComponentUpdate,根据返回值判断是否要继续更新。
  • 8.执行生命周期componentWillUpdate
  • 9.执行真正的更新,render
  • 10.执行生命周期componentDidUpdate

setState源码世界

相信能到这里的同学都知道了setState()是个既能同步又能异步的方法了,那具体什么时候是同步的,什么时候是异步的?

去源码里面看实现是比较靠谱的方式。

1、如何快速查看react源码

上react的github仓库,直接clone下来

react-github仓库

git clone https://github.com/facebook/react.git

到目前我看为止,最新的版本是16.13.1,我选了15.6.0的代码

如何切换版本?

1、找到对应版本号

image.png

2、复制15.6.0的历史记录号

image.png

3、回滚

git reset --hard 911603b

如图,成功回滚到15.6.0版本

image.png

2、setState入口 => enqueueSetState

核心原则:既然是看源码,那当然就不是一行一行的读代码,而是看核心的思想,所以接下来的代码都只会放核心代码,旁枝末节只提一下或者忽略setState的入口文件在src/isomorphic/modern/class/ReactBaseClasses.jsReact组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法

ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

partialState顾名思义-“部分state”,这取名,大概就是想不影响原来的state的意思吧
当调用setState时实际上是调用了enqueueSetState方法,我们顺藤摸瓜(我用的是vscode的全局搜索),找到了这个文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

image.png

这个文件导出了一个ReactUpdateQueue对象,“react更新队列”,代码名字起的好可以自带注释,说的就是这种大作吧,在这里注册了enqueueSetState方法

3、enqueueSetState => enqueueUpdate

先看enqueueSetState的定义

enqueueSetState: function(publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );
    
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },

这里只需要关注internalInstance的两个属性:

  • _pendingStateQueue:待更新队列
  • _pendingCallbacks: 更新回调队列
    如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue,最后执行enqueueUpdate(internalInstance)

接下来看enqueueUpdatefunction

enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

它执行的是ReactUpdates的enqueueUpdate方法

var ReactUpdates = require('ReactUpdates');

这个文件刚好就在旁边src/renderers/shared/stack/reconciler/ReactUpdates.js。找到enqueueUpdate方法

var ReactUpdates = {
  /**
   * React references `ReactReconcileTransaction` using this property in order
   * to allow dependency injection.
   *
   * @internal
   */
  ReactReconcileTransaction: null,

  batchedUpdates: batchedUpdates,
  enqueueUpdate: enqueueUpdate,
  flushBatchedUpdates: flushBatchedUpdates,
  injection: ReactUpdatesInjection,
  asap: asap,
};

module.exports = ReactUpdates;

定义如下

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

这段代码对于理解setState非常重要

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

判断batchingStrategy.isBatchingUpdates。batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程,开始默认值为false

上面这句话的意思是:

如果处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中;如果不处于批量更新模式,对所有队列中的更新执行batchedUpdates方法,往下看下去就知道是用事务的方式批量的进行component的更新,事务在下面。

借用《深入React技术栈》Page167中一图

image.png

4、核心:batchedUpdates => 调用transaction

batchingStrategy.isBatchingUpdates又是怎么回事呢?看来它才是关键.

但是,batchingStrategy 对象并不好找,它是通过 injection 方法注入的,一番寻找,发现了 batchingStrategy 就是ReactDefaultBatchingStrategy。 src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js具体怎么找文件,又属于另一个范畴了,我们今天只专注 setState,其他的容后再说吧

相信部分同学看到这里已经有些迷糊了,没关系,再坚持一下,旁枝末节先不管,只知道我们找到了核心方法batchedUpdates,马上要胜利了,别放弃(我第一次看也是这样熬过来的,一遍不行就两遍,大不了看多几遍又如何)

先看批量更新策略-batchingStrategy,它到底是什么

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

module.exports = ReactDefaultBatchingStrategy;

终于找到了,isBatchingUpdates属性和batchedUpdates方法如果isBatchingUpdates为true,当前正处于更新事务状态中,则将Component存入dirtyComponent中,否则调用batchedUpdates处理,发起一个transaction.perform()

注:所有的 batchUpdate 功能都是通过执行各种 transaction 实现的这是事务的概念,先了解一下事务吧

5、Transaction(事务)

这一段就直接引用书本里面的概念吧,《深入React技术栈》Page169

image.png

简单地说,一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

下面这段代码应该能帮助理解

var Transaction = require('./Transaction');

// 我们自己定义的 Transaction
var MyTransaction = function() {
  // do sth.
  this.reinitializeTransaction();
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function() {
    return [{
      initialize: function() {
        console.log('before method perform');
      },
      close: function() {
        console.log('after method perform');
      }
    }];
  };
});

var transaction = new MyTransaction();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform

看了上面的代码,如果还没有了解transaction.。没关系。可以看一下这篇文章,写的非常详细
React transaction完全解读

6、核心分析:batchingStrategy 批量更新策略

回到batchingStrategy:批量更新策略,再看看它的代码实现

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程

还记得上面说的很重要的那段代码吗

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

1、首先,点击事件的处理本身就是在一个大的事务中(这个记着就好),isBatchingUpdates已经是true了

2、调用setState()时,调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理

3、在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中;

4、在事务结束的时候才通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将批量执行关闭结束事务。

到这里我并没有顺着ReactUpdates.flushBatchedUpdates方法讲下去,这部分涉及到渲染和Virtual Dom的内容,反正你知道它是拿来执行渲染的就行了。

到这里为止,setState的核心概念已经比较清楚了,再往下的内容,暂时先知道就行了,不然展开来讲一环扣一环太杂了,我们做事情要把握核心。

到这里不知道有没有同学想起一个问题

isBatchingUpdates 标志位在 batchedUpdates 发起的时候被置为 true ,那什么时候被复位为false的呢?

还记得上面的事务的close方法吗,同一个文件src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

var transaction = new ReactDefaultBatchingStrategyTransaction();
// 定义复位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定义批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

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

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

相信眼尖的同学已经看到了,close的时候复位,把isBatchingUpdates设置为false。

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

var transaction = new ReactDefaultBatchingStrategyTransaction();

通过原型合并,事务的close 方法,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新

到这里,我们会发现,前面所有的队列、batchUpdate等等都是为了来到事务的这一步,前面都只是批收集的工作,到这里才真正的完成了批更新的操作。

当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

说了这么多 Transaction,关于上文提到的RESET_BATCHED_UPDATES主要用来管理isBatchingUpdates状态这句话是不是;理解更透彻了呐?

上文提到了两个wrapper:RESET_BATCHED_UPDATES和FLUSH_BATCHED_UPDATES。RESET_BATCHED_UPDATES用来管理isBatchingUpdates状态,我们前面在分析setState是否立即生效时已经讲解过了。那FLUSH_BATCHED_UPDATES用来干嘛呢?

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

var flushBatchedUpdates = function () {
  // 循环遍历处理完所有dirtyComponents
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // close前执行完runBatchedUpdates方法,这是关键
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

FLUSH_BATCHED_UPDATES会在一个transaction的close阶段运行runBatchedUpdates,从而执行update。

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

  for (var i = 0; i < len; i++) {
    // dirtyComponents中取出一个component
    var component = dirtyComponents[i];

    // 取出dirtyComponent中的未执行的callback,下面就准备执行它了
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      if (component._currentElement.props === component._renderedComponent._currentElement) {
        namedComponent = component._renderedComponent;
      }
    }
    // 执行updateComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    // 执行dirtyComponent中之前未执行的callback
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

runBatchedUpdates循环遍历dirtyComponents数组,主要干两件事。首先执行performUpdateIfNecessary来刷新组件的view,然后执行之前阻塞的callback。下面来看performUpdateIfNecessary。

performUpdateIfNecessary: function (transaction) {
  if (this._pendingElement != null) {
    // receiveComponent会最终调用到updateComponent,从而刷新View
    ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
  }

  if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
    // 执行updateComponent,从而刷新View。这个流程在React生命周期中讲解过
    this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
  }
}

最后惊喜的看到了receiveComponent和updateComponent吧。receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。 从而完成组件更新的整套流程。

updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
  ) {
    var inst = this._instance;
    invariant(
      inst != null,
      'Attempted to update component `%s` that has already been unmounted ' +
        '(or failed to mount).',
      this.getName() || 'ReactCompositeComponent',
    );

    var willReceive = false;
    var nextContext;

    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;

    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }

    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
        inst.componentWillReceiveProps(nextProps, nextContext);
    }

    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;

    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
         shouldUpdate = inst.shouldComponentUpdate(
            nextProps,
            nextState,
            nextContext,
          );
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          shouldUpdate =
            !shallowEqual(prevProps, nextProps) ||
            !shallowEqual(inst.state, nextState);
        }
      }
    }
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext,
      );
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

  _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,最后再次执行钩子函数componentDidUpdate。

_updateRenderedComponent执行组件的render方法。
在文件/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js中,代码如下:

 /**
   * Call the component's `render` method and update the DOM accordingly.
   *
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
  _updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();

    var debugID = 0;

    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(
        prevComponentInstance,
        nextRenderedElement,
        transaction,
        this._processChildContext(context),
      );
    } else {
      var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      var nodeType = ReactNodeTypes.getType(nextRenderedElement);
      this._renderedNodeType = nodeType;
      var child = this._instantiateReactComponent(
        nextRenderedElement,
        nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
      );
      this._renderedComponent = child;

      var nextMarkup = ReactReconciler.mountComponent(
        child,
        transaction,
        this._hostParent,
        this._hostContainerInfo,
        this._processChildContext(context),
        debugID,
      );
      this._replaceNodeWithMarkup(
        oldHostNode,
        nextMarkup,
        prevComponentInstance,
      );
    }
  },

  /**
   * Overridden in shallow rendering.
   *
   * @protected
   */
  _replaceNodeWithMarkup: function(oldHostNode, nextMarkup, prevInstance) {
    ReactComponentEnvironment.replaceNodeWithMarkup(
      oldHostNode,
      nextMarkup,
      prevInstance,
    );
  },

  /**
   * @protected
   */
  _renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    var renderedElement;

    renderedElement = inst.render();
    return renderedElement;
  },

到目前为止,我们已经基本介绍完了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并不保证同步更新。

举个

u=4000503827,2702628120&fm=26&gp=0.jpg

如下代码:

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return 
{this.state.val}
} } // 结果就为 0, 0, 2, 3
  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
  • setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和- 钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
    所以基于上述结论,如果想要实现上述代码中 4 次 console.log 打印出来的 val 分别是1、2、3、4。可以实现如下:
setTimeout(() => {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 1

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 2

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 3

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 4
}, 0);

或者

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 1

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 2

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 3

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 4

setState 干了什么

1、合成事件中的setState

react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。
在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true。
按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue,将要更新的组件存入dirtyComponent。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount后会将isBranchUpdate设置为false。这时将执行之前累积的setState。

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新前的val --> 0
  }

  render() {
    return (
      
{`Counter is: ${this.state.val}`}
) } }

2、生命周期函数中的setState

整个生命周期中就是一个事物操作,所以标识位isBatchingUpdates = true,所以流程到了enqueueUpdate()时,实例对象都会加入到dirtyComponents 数组中

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 输出的还是更新前的值 --> 0
 }
  render() {
    return (
      
{`Counter is: ${this.state.val}`}
) } }

3、原生事件中的setState

原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件
原生事件绑定不会通过合成事件的方式处理,自然也不会进入更新事务的处理流程。setTimeout也一样,在setTimeout回调执行时已经完成了原更新组件流程,不会放入dirtyComponent进行异步更新,其结果自然是同步的。

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      
{`Counter is: ${this.state.val}`}
) } }

4、setTimeout中的setState

由执行机制看,setState本身并不是异步的,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。

在生命周期,根据event loop的模型,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,isBranchUpdate被设置为false,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 输出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      
{`Counter is: ${this.state.val}`}
) } }

5、批量更新

在 setState 的时候react内部会创建一个 updateQueue ,通过 firstUpdate 、 lastUpdate 、 lastUpdate.next 去维护一个更新的队列,在最终的 performWork 中,相同的key会被覆盖,只会对最后一次的 setState 进行更新
分别执行以下代码:

  componentDidMount() {
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
  }
  componentDidMount() {
    this.setState((preState) => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
    this.setState(preState => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
  }

执行结果:

1
1
2
2

说明:

1.直接传递对象的setstate会被合并成一次
2.使用函数传递state不会被合并

批量更新中State合并机制

我们看下流程中_processPendingState的代码,这个函数是用来合并state暂存队列的,最后返回一个合并后的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 = _assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      _assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }

    return nextState;
  },

我们只需要关注下面这段代码:

_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);

如果传入的是对象,很明显会被合并成一次:

Object.assign(
  nextState,
  {index: state.index+ 1},
  {index: state.index+ 1}
)

如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。

总结

setState流程还是很复杂的,设计也很精巧,避免了重复无谓的刷新组件。它的主要流程如下:

  1. enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component;

2.如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。

3.batchedUpdates发起一次transaction.perform()事务;

4.开始执行事务初始化,运行,结束三个阶段;

初始化:事务初始化阶段没有注册方法,故无方法要执行;
运行:执行setSate时传入的callback方法,一般不会传callback参数;
结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法。
5.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。

你可能感兴趣的:(【React进阶系列】 setState机制)