圖解setState

表示很不蛋定發表於2017-12-20

研究 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

簡要的說一下流程:

  • 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,因此進入了死迴圈。

相關文章