阅读 2272

图解setState

研究 setState 这个问题来源于一个疑惑:使用 redux 的时候 dispatch 一个 action,为什么可以导致视图的更新?

首先的猜想是 store 改变后,redux 在某处调用了 setState,通知了 react。

看了下代码发现确实如此,调用 dispatch action 会触发一个 onStateChange 的函数 (这个函数在 connect 的时候就被注册到 store 了, storereducer 修改后触发),onStateChange 函数判断如果需要 shouldComponentUpdate 的话则执行 this.setState({}) 来触发 react 更新。

那么问题来了:

  1. 为什么 setState 可以让视图更新,它是如何一步步到 virtualDOM 然后渲染的呢
  2. setState为什么有时表现是异步的有时又是同步的?
  3. 为什么在生命周期函数中,willReceiveProps里可以setStatewillUpdate不行?

捋了一下流程得出下图,图中每个流程块冒号前即为被执行的函数:

简要的说一下流程:

  • setState 后将传入的 state 放入队列 queueenqueueUpdate 方法会根据 isBatchingUpdate 标志位判断,若当前已经在更新组件则将直接当前组件放入 dirtyComponents 数组,否则将 isBatchingUpdate 置为 true 并开启一个 "批量更新 (batchedUpdates)" 的事务(transaction)。

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

事务开启后会依次执行 initialize、perform、close 方法。可以看到,batchedUpdatesperform 阶段会再次执行 enqueueUpdate 方法,由于这时的 isBatchingUpdate 已经是 true 了所以会将当前组件放入 dirtyComponents。关键就在 close 阶段了,如果 dirtyComponents 为空则表示不需要更新,否则就开始更新,开启 flushBatchedUpdates 事务。

  • flushBatchedUpdatesperform 阶段会将 dirtyComponents 中的组件按 父 > 子 组件的顺序调用更新方法,组件在更新的时候会依次执行:
willReceiveProps -> 将 queue 中缓存的 state 与缓存的 state 合并 -> shouldComponentUpdate。
复制代码

如果判断需要更新,则执行组件的 render 方法得到新的 reactElement,将其与之前的 reactElement 做 diff 即可,将 diff 结果(删除,移动等)通过 setInnerHTML 等封装方法更新视图即可,细节可见图。

  • flushBatchedUpdatesclose 阶段会再次检查 dirtyComponents 长度有没有变化,如果变化了说明存在有新的 dirtyComponent,需要再来一次 flushBatchedUpdates

补上 updateComponent 代码:

// 更新组件
updateComponent: function(transaction, prevParentElement, nextParentElement) {
  var prevContext = this.context;
  var prevProps = this.props;
  var nextContext = prevContext;
  var nextProps = prevProps;

  if (prevParentElement !== nextParentElement) {
    nextContext = this._processContext(nextParentElement._context);
    nextProps = this._processProps(nextParentElement.props);
    // 当前状态为 RECEIVING_PROPS
    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;

    // 如果存在 componentWillReceiveProps,则执行
    if (this.componentWillReceiveProps) {
      this.componentWillReceiveProps(nextProps, nextContext);
    }
  }

  // 设置状态为 null,更新 state
  this._compositeLifeCycleState = null;
  var nextState = this._pendingState || this.state;
  this._pendingState = null;
  var shouldUpdate =
    this._pendingForceUpdate ||
    !this.shouldComponentUpdate ||
    this.shouldComponentUpdate(nextProps, nextState, nextContext);
  if (!shouldUpdate) {
    // 如果确定组件不更新,仍然要设置 props 和 state
    this._currentElement = nextParentElement;
    this.props = nextProps;
    this.state = nextState;
    this.context = nextContext;
    this._owner = nextParentElement._owner;
    return;
  }
  this._pendingForceUpdate = false;

  ......

  // 如果存在 componentWillUpdate,则触发
  if (this.componentWillUpdate) {
    this.componentWillUpdate(nextProps, nextState, nextContext);
  }

  // render 递归渲染
  var nextMarkup = this._renderedComponent.mountComponent(
    thisID,
    transaction,
    this._mountDepth + 1
  );

  // 如果存在 componentDidUpdate,则触发
  if (this.componentDidUpdate) {
    transaction.getReactMountReady().enqueue(
      this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),
      this
    );
  }
},
复制代码

捋完整个流程可以回答之前一些疑惑:

  1. 为什么 setState 后紧接着打 log,有时 state 没有立刻变,有时候又变了?

生命周期中的 setState 处于一个大的 transaction 中,此时的 isBatchingUpdatetrue,执行 setState 只会让 dirtyComponents 数组 push 当前组件而不会进一步处理,此时 log 来看的话 state 还是没有变的。而如果在 transaction 之外,例如 setTimeoutsetState,此时 isBatchingUpdatefalse,会一路直接执行下来更改 state,所以此时 log 出来 state 是被立刻改变了的。因此 setState 不保证是同步,而不是说它一定是异步

2. 都在同一个 tranaction 中,为什么在 willReceiveProps 时还可以 setState,而在 shouldComponentUpdatewillUpdate 的时候 setState 会导致浏览器死循环?

组件内部有一标志位 _compositeLifeCycleState 表示当前生命周期状态,在 willReceiveProps 前被设置为 RECEIVING_PROPS,在 willReceiveProps 执行后被设置为 null,而 performUpdateIfNecessary 函数在当前状态为 MOUNTINGRECEIVING_PROPS 时不会继续调用 updateComponent 函数。

performUpdateIfNecessary: function(transaction) {
  var compositeLifeCycleState = this._compositeLifeCycleState;
  //  ■■■■■■■■重点■■■■■■■■■■■■
  // 当状态为 MOUNTING 或 RECEIVING_PROPS 时,则不更新
  if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
      compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
    return;
  }

  var prevElement = this._currentElement;
  var nextElement = prevElement;
  if (this._pendingElement != null) {
    nextElement = this._pendingElement;
    this._pendingElement = null;
  }

  // 调用 updateComponent
  this.updateComponent(
    transaction,
    prevElement,
    nextElement
  );
}
复制代码

因此在 willReceivePropssetState 由于 _compositeLifeCycleState 已经是 RECEIVING_PROPS 了,不回触发新的 updateComponent,而在 willUpdate 的时候 _compositeLifeCycleState 已经被置回 null 了,因此会引发下一次的 updateComponent,然后就再次触发组件的各生命周期,当然也会免不了执行 willUpdate,因此进入了死循环。