把 setState 整明白

加入新團隊後,團隊項目使用了React Native。剛開始接觸React Native,除了學習React Native的使用,更要了解React.js這個框架,才能更好的使用。而React框架中,筆者一開始就感受奇妙的,就是這個看似同步,表現卻不必定是同步的setState方法。看了網上一些文章,結論彷佛都是從某幾篇博客相互借鑑的結論,但裏面筆者仍是以爲有一些不太明白的地方,幸好React.js源碼是開源的。順着源碼看下去,一些困惑的問題終於有些眉目。javascript

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

  • setState是異步仍是同步的
  • 在setTimeout方法中調用setState的值爲什麼立刻就能更新
  • setState中傳入一個Function爲什麼值立刻就能更新
  • setState爲什麼要如此設計
  • setState的最佳實踐是什麼

如下源碼基於咱們團隊在用的React 16.0.0版本,目前最新的React 16.4.0版本的類名和文件結構均有很大變化,但設計思想應該仍是差很少的,可供參考。react

setState的入口

setState的最上層天然在ReactBaseClasses中。git

//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
  // ...
  //調用內部updater
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

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

//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方法的內容。app

//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爲何不會同步更新組件狀態

相關文章
相關標籤/搜索