把 setState 整明白

Nemocdz發表於2019-02-07

加入新團隊後,團隊專案使用了React Native。剛開始接觸React Native,除了學習React Native的使用,更要了解React.js這個框架,才能更好的使用。而React框架中,筆者一開始就感覺奇妙的,就是這個看似同步,表現卻不一定是同步的setState方法。看了網上一些文章,結論似乎都是從某幾篇部落格相互借鑑的結論,但裡面筆者還是覺得有一些不太明白的地方,幸虧React.js原始碼是開源的。順著原始碼看下去,一些困惑的問題終於有些眉目。

開始之前,讀者可思考以下幾個問題:

  • setState是非同步還是同步的
  • 在setTimeout方法中呼叫setState的值為何馬上就能更新
  • setState中傳入一個Function為何值馬上就能更新
  • setState為何要如此設計
  • setState的最佳實踐是什麼

以下原始碼基於我們團隊在用的React 16.0.0版本,目前最新的React 16.4.0版本的類名和檔案結構均有很大變化,但設計思想應該還是差不多的,可供參考。

setState的入口

setState的最上層自然在ReactBaseClasses中。

//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
  // ...
  //呼叫內部updater
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製程式碼

而這個updater,在初始化時已經告訴我們,是實際使用時注入的。

//ReactBaseClasses.js
function ReactComponent(props, context, updater) {
  // ...
  // 真正的updater在renderer注入
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}
複製程式碼

找到注入的地方,目的是找到updater是個什麼型別。

//ReactCompositeComponet.js
mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
        // ...
        // 由Transaction獲取,這個是ReactReconcileTransaction
        var updateQueue = transaction.getUpdateQueue();
        // ...
        inst.updater = updateQueue;
        // ...
    }
複製程式碼
//ReactReconcileTransaction.js
getUpdateQueue: function() {
    return ReactUpdateQueue;
},
複製程式碼

終於看到了具體enqueSetState方法的內容。

//ReactUpdateQueue.js
enqueueSetState: function(
    publicInstance,
    partialState,
    callback,
    callerName,
  ) {
    // ...
    // 外部示例轉化為內部例項
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
    // ...
    // 將需要更新的state放入等待佇列中
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    // ...
    // callback也一樣放入等待佇列中
    if (callback !== null) {
      // ...
      if (internalInstance._pendingCallbacks) {
        internalInstance._pendingCallbacks.push(callback);
      } else {
        internalInstance._pendingCallbacks = [callback];
      }
    }

    enqueueUpdate(internalInstance);
  },
      
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}
複製程式碼

而更新操作由ReactUpdates這個類負責。

//ReactUpdates.js
function enqueueUpdate(component) {
  //...

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

  dirtyComponents.push(component);
  //...
}
複製程式碼

而這個isBatchingUpdates的判斷,就是代表是否在批量更新中。如果正在更新中,則整個元件放入dirtyComponents陣列中,後面會講到。這裡這個batchingStrategy,其實就是ReactDefaultBatchingStrategy(外部注入的)。

//ReactDOMStackInjection.js
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
複製程式碼

而這個類裡的,則會讓掛起更新狀態,並呼叫transaction的perform。

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

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 正常情況不會走入if中
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};
複製程式碼

Transaction

這裡簡單解釋下事務(Transaction)的概念,先看原始碼中對事務的一張解釋圖。

 //Transaction.js
 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

複製程式碼

簡單來說,事務相當於對某個方法(anyMethod)執行前和執行後的多組鉤子的集合。

可以方便的在某個方法前後分別做一些事情,而且可以分wrapper定義,一個Wrapper一對鉤子。

具體來說,可以在Wrapper裡定義initialize和close方法,initialize會在anyMethod執行前執行,close會在執行後執行。

更新策略裡的Transaction

回到剛剛的batchedUpdates方法,裡面那個transaction其實執行前都是空方法,而callback是外界傳入的enqueueUpdate方法本身,也就是說,執行時會被isBatchingUpdates卡住進入加入dirtyCompoments中。之後就會執行close方法裡面去改變isBatchingUpdates的值和執行flushBatchedUpdates方法。

//ReactDefaultBatchingStrategy.js

// 更新狀態isBatchingUpdates的wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

// 真正更新狀態的wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

// 兩個wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

// 新增transaction的wrappers
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

複製程式碼

而這個flushBatchedUpdates方法,按照dirtyComonents裡的數量,每次執行了一個transaction。

//ReactUpdates.js
var flushBatchedUpdates = function() {
  while (dirtyComponents.length) {
    var transaction = ReactUpdatesFlushTransaction.getPooled();
    transaction.perform(runBatchedUpdates, null, transaction);
    ReactUpdatesFlushTransaction.release(transaction);
  }
};

複製程式碼

而這個transaction的執行前後前後的鉤子如下。

//ReactUpdtes.js
// 開始時同步dirComponents數量,結束時通過檢查是否在執行中間runBatchedUpdates方法時還有新加入的component,有的話就重新執行一遍
var NESTED_UPDATES = {
  initialize: function() {
    this.dirtyComponentsLength = dirtyComponents.length;
  },
  close: function() {
    if (this.dirtyComponentsLength !== dirtyComponents.length) {
      dirtyComponents.splice(0, this.dirtyComponentsLength);
      flushBatchedUpdates();
    } else {
      dirtyComponents.length = 0;
    }
  },
};

var TRANSACTION_WRAPPERS = [NESTED_UPDATES];


//新增wrapper
Object.assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

複製程式碼

所以真正更新方法應該在runBatchedUpdates中。

//ReactUpdates.js
function runBatchedUpdates(transaction) {
  
  // 排序,保證父元件比子元件先更新
  dirtyComponents.sort(mountOrderComparator);

  // ...
  for (var i = 0; i < len; i++) {
    
    var component = dirtyComponents[i];
    
    //這裡開始進入更新元件的方法
    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction,
      updateBatchNumber,
    );
  }
}

複製程式碼

而ReactReconciler中的performUpateIfNecessary方法只是一個殼。

performUpdateIfNecessary: function(
    internalInstance,
    transaction,
    updateBatchNumber,
  ) {
    // ...
    internalInstance.performUpdateIfNecessary(transaction);
    // ...
 },

複製程式碼

而真正的方法在ReactCompositeComponent中,如果等待佇列中有該更新的state,那麼就呼叫updateComponent。

//ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
    // ...
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context,
      );
    } else {
	// ...
    }
},

複製程式碼

這個方法判斷了做了一些判斷,而我們也看到了nextState的值才是最後被更新給state的值。

//ReactCompositeComponent.js
updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
) {
    // 這個將排序佇列裡的state合併到nextState    
   	var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;
        
    if (shouldUpdate) {
    //...
    } else {
      // 這裡才正式更新state
      //...
      inst.state = nextState;
      //...
    }
}

複製程式碼

這個方法也解釋了為什麼傳入函式的state會更新。

_processPendingState: function(props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    
    //更新了就可以置空了
    this._pendingStateQueue = null;

    
    var nextState = replace ? queue[0] : inst.state;
    var dontMutate = true;
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      //如果setState傳入是函式,那麼接收的state是上輪更新過的state
      var partial = queue[i];
      let partialState = typeof partial === 'function'
        ? partial.call(inst, nextState, props, context)
        : partial;
      if (partialState) {
        if (dontMutate) {
          dontMutate = false;
          nextState = Object.assign({}, nextState, partialState);
        } else {
          Object.assign(nextState, partialState);
        }
      }
    }
    return nextState;
},

複製程式碼

好像setState是同步的耶

而如果按照這個流程看完,setState應該是同步的呀?是哪裡出了問題呢。

別急,還記得更新策略裡面那個Transaction麼。那裡中間呼叫的callback是外層傳入的,也就說有可能還有其它呼叫了batchedUpdates呢。那麼也就是說,中間的callback,並不止setState會引起。在程式碼裡搜尋後發現,果真還有幾處呼叫了batchedUpdates方法。

比如ReactMount的這兩個方法

//ReactMount.js
_renderNewRootComponent: function(
  nextElement,
  container,
  shouldReuseMarkup,
  context,
  callback,
) {
    
    // ...
    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.

    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );
     
    // ...
},
      
unmountComponentAtNode: function(container) {
    // ...   
    ReactUpdates.batchedUpdates(
      unmountComponentFromNode,
      prevComponent,
      container,
    );
    return true;
    // ...
},      

複製程式碼

比如ReactDOMEventListener

//ReactDOMEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {   
    // ...
    try {
      // Event queue being processed in the same cycle allows
      // `preventDefault`.
      ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
     // ...
    }
},
      
//ReactDOMStackInjection.js     
ReactGenericBatching.injection.injectStackBatchedUpdates(
  ReactUpdates.batchedUpdates,
);

複製程式碼

所以比如在componentDidMount中直接呼叫時,ReactMount.js 中的**_renderNewRootComponent** 方法已經呼叫了,也就是說,整個將 React 元件渲染到 DOM 中的過程就處於一個大的 Transaction 中,而其中的callback沒有馬上被執行,那麼自然state沒有被馬上更新。

setState為什麼這麼設計

在react中,state代表UI的狀態,也就是UI由state改變而改變,也就是UI=function(state)。筆者覺得,這體現了一種響應式的思想,而響應式與命令式的不同,在於命令式著重看如何命令的過程,而響應式看中資料變化如何輸出。而React中對Rerender做出的努力,對渲染的優化,響應式的setState設計,其實也是其中搭配而不可少的一環。

最後

最前面的問題,相信每個人都有自己的答案,我這裡給出我自己的理解。

Q:setState是非同步還是同步的?

A:同步的,但有時候是非同步的表現。

Q:在setTimeout方法中呼叫setState的值為何馬上就能更新?

A:因為本身就是同步的,也沒有別的因素阻塞。

Q:setState中傳入一個Function為何值馬上就能更新?

A:原始碼中的策略。

Q:setState為何要如此設計?

A:為了以響應式的方式改變UI。

Q:setState的最佳實踐是什麼?

A:以響應式的思路使用。

參考連結

React 原始碼剖析系列 - 解密 setState

setState:這個API設計到底怎麼樣

setState為什麼不會同步更新元件狀態

相關文章