圖解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,所以進入了死循環。

相關文章
相關標籤/搜索