图解setState

研究 setState 这个问题来源于一个疑惑:使用 redux 的时候 dispatch 一个 action,为何能够致使视图的更新?react

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

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

那么问题来了:浏览器

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

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

简要的说一下流程:bash

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

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

事务开启后会依次执行 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 代码:ui

// 更新组件
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,所以进入了死循环。

相关文章
相关标签/搜索