一個關於React.Component.setState的問題

React組件從新渲染的條件是:
B.只要調用this.setState()就會發生從新渲染。
C.必須調用this.setState()且傳遞不一樣於當前this.setState()的參數,纔會引起從新渲染。

本文將從三方面說明這個問題爲何選擇C。或者說爲何 setState 在傳遞不一樣當前 this.State 的參數,纔會引起組件從新渲染。javascript

結論

我仍是想選擇Bhtml

引用規範

TL;DRjava

下面是 React 官方對於 setState 的說明,翻譯的做者是我。在這段文章中,對setState說明了兩點。react

  1. setState是異步的。
  2. setState會(always)致使從新渲染,當且僅當shouldComponentUpdate()返回了false的時候不會。

讀者能夠直接進入實驗部分。git

React原文中關於setState的說明:github

setState(updater[, callback])

setState() enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state. This is the primary method you use to update the user interface in response to event handlers and server responses.架構

setState() 會將當前組件的 state 的更改所有推入隊列,而且通知 React 這個組件和他的孩子們須要更新這些狀態並從新渲染。這是開發者常用的用來更新 UI 的方法(不論是在事件響應中仍是處理服務端的返回)。app

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.less

setState()看成一個更新的_請求_而不是一個更新的函數。爲了更好的性能,React 可能會延遲這些更新,將幾個組件的更新合併在一塊兒執行。React不保證這個狀態的更新是當即執行的。dom

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

setState()並非當即更新這些組件,而是可能延後批量更新。這個問題致使若是開發者在setState()以後當即去訪問this.state可能訪問的不是最新的狀態。然而,開發者仍是可使用一些方法來訪問到最新的state的。好比在組件生命週期的componentDidUpdate,或者在setState的回調函數中。固然了若是你須要依賴以前的狀態來更新當前的狀態,看一看這個updater

setState() will always lead to a re-render unless shouldComponentUpdate() returns false. If mutable objects are being used and conditional rendering logic cannot be implemented in shouldComponentUpdate(), calling setState() only when the new state differs from the previous state will avoid unnecessary re-renders.

setState() 確定老是一直毫無疑問的會致使render函數被從新調用[1],除非shouldComponentUpdate()返回了false。若是開發者使用了可變的變量或者更新的邏輯沒法在shouldComponentUpdate()中編寫,那爲了減小無心義的從新渲染,應該僅僅在肯定當前的新狀態和舊狀態不同的時候調用setState()。【但願讀者不要誤會,React是讓開發者本身作這個比較。不是React替你作好了的。】

[1].(咱們把這種行爲叫作從新渲染)

The first argument is an updater function with the signature:

setState()能夠接受兩個參數,第一個參數叫作updater的函數,函數的簽名以下:

(prevState, props) => stateChange

prevState is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState and props. For instance, suppose we wanted to increment a value in state by props.step:

prevState是組件以前的狀態(引用關係)。prevState不該該被直接更改,而是應該新建一個Object來表示更新後的狀態。舉個例子:若是開發者想要更新state中的counter給它加一。應該按照下面的作法。

this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

Both prevState and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with prevState.

React 保證 updater 接受的 prevStateprops 都是最新的。而且updater 的返回是被淺拷貝merge進入老狀態的。

The second parameter to setState() is an optional callback function that will be executed once setState is completed and the component is re-rendered. Generally we recommend using componentDidUpdate() for such logic instead.

setState()的第二個參數是可選的回調函數。在state更新完成後他會被執行一次。整體上來講,React官方更推薦在componentDidUpdate()中來實現這個邏輯。

You may optionally pass an object as the first argument to setState() instead of a function:

開發者還能夠在第一次參數的位置不傳入函數,而是傳入一個對象。

setState(stateChange[, callback])

This performs a shallow merge of stateChange into the new state, e.g., to adjust a shopping cart item quantity:

像上面這種調用方式中,stateChange會被淺拷貝進入老狀態。例如開發者更新購物車中的商品數量的代碼應該以下所示:

this.setState({quantity: 2})

This form of setState() is also asynchronous, and multiple calls during the same cycle may be batched together. For example, if you attempt to increment an item quantity more than once in the same cycle, that will result in the equivalent of:

這種形式的setState()也是異步的,並且在一個週期內的屢次更新會被批量一塊兒更新。若是你想更新狀態裏面的數量,讓他一直加一加一。代碼就會以下所示

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

Subsequent calls will override values from previous calls in the same cycle, so the quantity will only be incremented once. If the next state depends on the previous state, we recommend using the updater function form, instead:

這樣隊列的調用會重寫以前的更新,因此最後數量僅僅會被更新一次。在這種新狀態依賴老狀態數據的狀況下,React官方推薦你們使用函數。以下所示:

this.setState((prevState) => {
  return {quantity: prevState.quantity + 1};
});

實驗驗證

設計實驗來驗證React官方說法的正確性。實驗採用基於控制變量法的對照試驗。

基於 React16( 引入了 Fiber 架構)和 React 0.14 分別進行實驗。至於React 15的問題,留給讀者本身吧。

編寫以下組件代碼:

class A extends React.Component{
    constructor(props){
        super(props);
        this.state = {
            a:1
        }
        this._onClick = this.onClick.bind(this);
    }
    onClick(){
        this.setState({a:2}) // 替換點
    }
    render(){
        console.log('rerender');
        return(
            <div onClick={this._onClick}>
                <p>a: {this.state.a}</p>
                <p>{Math.random()}</p>
            </div>
        );
    }
}

若是須要能夠讀者自行粘貼從新復現實驗。

更新的標準:界面中顯示的隨機數是否發生了變化。固然也能夠觀察 Console中是否出現了 rerender

React 0.14.5 實驗結果以下所示:

條件 不編寫shouldComponentUpdate()方法 return false; return true;
setState({}) 更新 不更新 更新
setState(null) 更新 不更新 更新
setState(undefined) 更新 不更新 更新
setState(this.state) 更新 不更新 更新
setState(s=>s) 更新 不更新 更新
setState({a:2}) 更新 不更新 更新

React 16 實驗結果以下所示:

條件 不編寫shouldComponentUpdate()方法 return false; return true;
setState({}) 更新 不更新 更新
setState(null) 不更新 不更新 不更新
setState(undefined) 不更新 不更新 不更新
setState(this.state) 更新 不更新 更新
setState(s=>s) 更新 不更新 更新
setState({a:2}) 更新 不更新 更新

可見對於setState()來講,React 在不一樣版本的表現不盡相同。

React 0.14中可能更符合只要調用setState()就會進行更新。

React 16.3.2中只有在傳遞null和undefined的時候纔不會更新,別的時候都更新。

源碼說明

React 16中是這樣的:

https://github.com/facebook/r...

1. const payload = update.payload;
2. let partialState;
3. if (typeof payload === 'function') {
4.       partialState = payload.call(instance, prevState, nextProps);
5. } else {
6.       // Partial state object
7.       partialState = payload;
8. }
9. if (partialState === null || partialState === undefined) {
10.   // Null and undefined are treated as no-ops.
11.   return prevState;
12.}
13.// Merge the partial state and the previous state.
14.return Object.assign({}, prevState, partialState);

React 14中是這樣的:

證有容易,證無難,因此我要順着整條鏈路的源碼的展現一遍。

TL;DR

var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
   var partial = queue[i];
   assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;

流程中沒有任何比較操做。

1.調用

setState({})

2.原型方法

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};

3.入隊方法

enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    if (!internalInstance) {
      return;
    }
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    enqueueUpdate(internalInstance);
  },

internalInstance 是一個 ReactCompositeComponentWrapper,大概就是包裝着ReactComponent實例的一個對象。

4.入隊

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

5.更具當前的批量策略來決定更新方法

function enqueueUpdate(component) {
  ensureInjected();

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

  dirtyComponents.push(component);
}

6.能夠看到直到這裏都沒人管這個東西到底更新的是什麼。
7.剩下的事情基本就是垃圾回收處理現場的事情了。
8.處理完以後會

ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);

9.請求更新隊列,進行更新

var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);<!--here-->
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

10.更新

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;

  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.
  dirtyComponents.sort(mountOrderComparator);

  for (var i = 0; i < len; i++) {
    // If a component is unmounted before pending changes apply, it will still
    // be here, but we assume that it has cleared its _pendingCallbacks and
    // that performUpdateIfNecessary is a noop.
    var component = dirtyComponents[i];

    // If performUpdateIfNecessary happens to enqueue any new updates, we
    // shouldn't execute the callbacks until the next render happens, so
    // stash the callbacks first
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;
<!--here-->
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

11.最重要的來了

performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(this, this._pendingElement || this._currentElement, transaction, this._context);
    }

    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    }
  },

12.更新

updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
    //... props context 更新
    var nextState = this._processPendingState(nextProps, nextContext);

    var shouldUpdate = this._pendingForceUpdate || !inst.shouldComponentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext);

    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Will set `this.props`, `this.state` and `this.context`.
      this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext);
    } else {
      // If it's determined that a component should not update, we still want
      // to set props and state but we shortcut the rest of the update.
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
  },

13.計算state

_processPendingState: function (props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
    }

    return nextState;
  },

14.就這樣了。

var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
   var partial = queue[i];
   assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;

15.流程中沒有任何比較操做。