【React進階系列】 setState機制

timg (4).jpg

api解析: setState(updater, [callback])


updater: 更新數(shù)據(jù) FUNCTION/OBJECT
callback: 更新成功后的回調(diào) FUNCTION
// updater - Function
this.setState((prevState, props) => {
  return {counter: prevState.counter + props.step};
});

// update - Object
this.setState({quantity: 2})

setState的特點:


1.異步:react通常會集齊一批需要更新的組件,然后一次性更新來保證渲染的性能
2.淺合并 Objecr.assign()

setState問題與解決

舉個??


  • 在使用setState改變狀態(tài)之后,立刻通過this.state去拿最新的狀態(tài)
    解決: componentDidUpdate或者setState的回調(diào)函數(shù)里獲取
// setState回調(diào)函數(shù)
changeTitle: function (event) {
  this.setState({ title: event.target.value }, () => this.APICallFunction());
},
APICallFunction: function () {
  // Call API with the updated value
}
  • 有一個需求,需要在在onClick里累加兩次,使用對象的方法更新,則只會加一次
    解決: 使用updater function
onClick = () => {
    this.setState({ index: this.state.index + 1 });
    this.setState({ index: this.state.index + 1 });
}

// 最后解析為,后面的數(shù)據(jù)會覆蓋前面的更改,所以最終只加了一次.
Object.assign(
  previousState,
  {index: state.index+ 1},
  {index: state.index+ 1},
)

//正確寫法
onClick = () => {
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
    this.setState((prevState, props) => {
      return {quantity: prevState.quantity + 1};
    });
}

注意:

1.不要在render()函數(shù)里面寫setstate(),除非你自己定制了shouldComponentUpdate方法,要不然會引起無限循環(huán)

render() {
    //this.setState
    return(
        //...dom
    )
}

2.不要給this.state直接復制
react為了實現(xiàn)高效render, state其實是一個隊列,setState是將數(shù)據(jù)插入隊列中,使用方式1直接賦值不會觸發(fā)渲染, react提供了setState的實例方法可以觸發(fā)render。

// 1
this.state.num = 1
// 2
this.setState({
    num: this.state.num + 1
})

3.對數(shù)組和對象等引用對象操作時,使用返回新對象的方法
array: 不要使用push、pop、shift、unshift、splice可使用concat、slice、filter、擴展語法
object: Object.assgin/擴展語法

setState更新機制


如圖:


3114633915-5c7108d209e99_articlex.png

圖不清楚可以點擊查看原圖

  • partialStatesetState傳入的第一個參數(shù),對象或函數(shù)
  • _pendingStateQueue:當前組件等待執(zhí)行更新的state隊列
  • isBatchingUpdates:react用于標識當前是否處于批量更新狀態(tài),所有組件公用
  • dirtyComponent:當前所有處于待更新狀態(tài)的組件隊列
  • transcation:react的事務機制,在被事務調(diào)用的方法外包裝n個waper對象,并一次執(zhí)行:waper.init、被調(diào)用方法、waper.close
  • FLUSH_BATCHED_UPDATES:用于執(zhí)行更新的waper,只有一個close方法

2.執(zhí)行過程

對照上面流程圖的文字說明,大概可分為以下幾步:

  • 1.將setState傳入的partialState參數(shù)存儲在當前組件實例的_pendingStateQueue中。
  • 2.判斷當前React是否處于批量更新狀態(tài),如果是,將當前組件標記為dirtyCompontent,并加入待更新的組件隊列中。
  • 3.如果未處于批量更新狀態(tài),將isBatchingUpdates設置為true,用事務再次調(diào)用前一步方法,保證當前組件加入到了待更新組件隊列中。
  • 4.調(diào)用事務的waper方法,遍歷待更新組件隊列依次執(zhí)行更新。
  • 5.執(zhí)行生命周期componentWillReceiveProps。
  • 6.將組件的state暫存隊列中的state進行合并,獲得最終要更新的state對象,并將_pendingStateQueue置為空。
  • 7.執(zhí)行生命周期shouldComponentUpdate,根據(jù)返回值判斷是否要繼續(xù)更新。
  • 8.執(zhí)行生命周期componentWillUpdate。
  • 9.執(zhí)行真正的更新,render。
  • 10.執(zhí)行生命周期componentDidUpdate

setState源碼世界

相信能到這里的同學都知道了setState()是個既能同步又能異步的方法了,那具體什么時候是同步的,什么時候是異步的?

去源碼里面看實現(xiàn)是比較靠譜的方式。

1、如何快速查看react源碼

上react的github倉庫,直接clone下來

react-github倉庫

git clone https://github.com/facebook/react.git

到目前我看為止,最新的版本是16.13.1,我選了15.6.0的代碼

如何切換版本?

1、找到對應版本號

image.png

2、復制15.6.0的歷史記錄號

image.png

3、回滾

git reset --hard 911603b

如圖,成功回滾到15.6.0版本

image.png

2、setState入口 => enqueueSetState

核心原則:既然是看源碼,那當然就不是一行一行的讀代碼,而是看核心的思想,所以接下來的代碼都只會放核心代碼,旁枝末節(jié)只提一下或者忽略setState的入口文件在src/isomorphic/modern/class/ReactBaseClasses.jsReact組件繼承自React.Component,而setState是React.Component的方法,因此對于組件來講setState屬于其原型方法

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

partialState顧名思義-“部分state”,這取名,大概就是想不影響原來的state的意思吧
當調(diào)用setState時實際上是調(diào)用了enqueueSetState方法,我們順藤摸瓜(我用的是vscode的全局搜索),找到了這個文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js

image.png

這個文件導出了一個ReactUpdateQueue對象,“react更新隊列”,代碼名字起的好可以自帶注釋,說的就是這種大作吧,在這里注冊了enqueueSetState方法

3、enqueueSetState => enqueueUpdate

先看enqueueSetState的定義

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

    enqueueUpdate(internalInstance);
  },

這里只需要關注internalInstance的兩個屬性:

  • _pendingStateQueue:待更新隊列
  • _pendingCallbacks: 更新回調(diào)隊列
    如果_pendingStateQueue的值為null,將其賦值為空數(shù)組[],并將partialState放入待更新state隊列_pendingStateQueue,最后執(zhí)行enqueueUpdate(internalInstance)

接下來看enqueueUpdatefunction

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

它執(zhí)行的是ReactUpdates的enqueueUpdate方法

var ReactUpdates = require('ReactUpdates');

這個文件剛好就在旁邊src/renderers/shared/stack/reconciler/ReactUpdates.js。找到enqueueUpdate方法

var ReactUpdates = {
  /**
   * React references `ReactReconcileTransaction` using this property in order
   * to allow dependency injection.
   *
   * @internal
   */
  ReactReconcileTransaction: null,

  batchedUpdates: batchedUpdates,
  enqueueUpdate: enqueueUpdate,
  flushBatchedUpdates: flushBatchedUpdates,
  injection: ReactUpdatesInjection,
  asap: asap,
};

module.exports = ReactUpdates;

定義如下

function enqueueUpdate(component) {
  ensureInjected();

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

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

這段代碼對于理解setState非常重要

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

判斷batchingStrategy.isBatchingUpdates。batchingStrategy是批量更新策略,isBatchingUpdates表示是否處于批量更新過程,開始默認值為false

上面這句話的意思是:

如果處于批量更新模式,也就是isBatchingUpdates為true時,不進行state的更新操作,而是將需要更新的component添加到dirtyComponents數(shù)組中;如果不處于批量更新模式,對所有隊列中的更新執(zhí)行batchedUpdates方法,往下看下去就知道是用事務的方式批量的進行component的更新,事務在下面。

借用《深入React技術?!稰age167中一圖

image.png

4、核心:batchedUpdates => 調(diào)用transaction

batchingStrategy.isBatchingUpdates又是怎么回事呢?看來它才是關鍵.

但是,batchingStrategy 對象并不好找,它是通過 injection 方法注入的,一番尋找,發(fā)現(xiàn)了 batchingStrategy 就是ReactDefaultBatchingStrategy。 src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js具體怎么找文件,又屬于另一個范疇了,我們今天只專注 setState,其他的容后再說吧

相信部分同學看到這里已經(jīng)有些迷糊了,沒關系,再堅持一下,旁枝末節(jié)先不管,只知道我們找到了核心方法batchedUpdates,馬上要勝利了,別放棄(我第一次看也是這樣熬過來的,一遍不行就兩遍,大不了看多幾遍又如何)

先看批量更新策略-batchingStrategy,它到底是什么

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

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

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

module.exports = ReactDefaultBatchingStrategy;

終于找到了,isBatchingUpdates屬性和batchedUpdates方法如果isBatchingUpdates為true,當前正處于更新事務狀態(tài)中,則將Component存入dirtyComponent中,否則調(diào)用batchedUpdates處理,發(fā)起一個transaction.perform()

注:所有的 batchUpdate 功能都是通過執(zhí)行各種 transaction 實現(xiàn)的這是事務的概念,先了解一下事務吧

5、Transaction(事務)

這一段就直接引用書本里面的概念吧,《深入React技術?!稰age169

image.png

簡單地說,一個所謂的 Transaction 就是將需要執(zhí)行的 method 使用 wrapper 封裝起來,再通過 Transaction 提供的 perform 方法執(zhí)行。而在 perform 之前,先執(zhí)行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 執(zhí)行后)再執(zhí)行所有的 close 方法。一組 initialize 及 close 方法稱為一個 wrapper,從上面的示例圖中可以看出 Transaction 支持多個 wrapper 疊加。

具體到實現(xiàn)上,React 中的 Transaction 提供了一個 Mixin 方便其它模塊實現(xiàn)自己需要的事務。而要使用 Transaction 的模塊,除了需要把 Transaction 的 Mixin 混入自己的事務實現(xiàn)中外,還需要額外實現(xiàn)一個抽象的 getTransactionWrappers 接口。這個接口是 Transaction 用來獲取所有需要封裝的前置方法(initialize)和收尾方法(close)的,因此它需要返回一個數(shù)組的對象,每個對象分別有 key 為 initialize 和 close 的方法。

下面這段代碼應該能幫助理解

var Transaction = require('./Transaction');

// 我們自己定義的 Transaction
var MyTransaction = function() {
  // do sth.
  this.reinitializeTransaction();
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function() {
    return [{
      initialize: function() {
        console.log('before method perform');
      },
      close: function() {
        console.log('after method perform');
      }
    }];
  };
});

var transaction = new MyTransaction();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform

看了上面的代碼,如果還沒有了解transaction.。沒關系??梢钥匆幌逻@篇文章,寫的非常詳細
React transaction完全解讀

6、核心分析:batchingStrategy 批量更新策略

回到batchingStrategy:批量更新策略,再看看它的代碼實現(xiàn)

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

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

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

可以看到isBatchingUpdates的初始值是false的,在調(diào)用batchedUpdates方法的時候會將isBatchingUpdates變量設置為true。然后根據(jù)設置之前的isBatchingUpdates的值來執(zhí)行不同的流程

還記得上面說的很重要的那段代碼嗎

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

1、首先,點擊事件的處理本身就是在一個大的事務中(這個記著就好),isBatchingUpdates已經(jīng)是true了

2、調(diào)用setState()時,調(diào)用了ReactUpdates.batchedUpdates用事務的方式進行事件的處理

3、在setState執(zhí)行的時候isBatchingUpdates已經(jīng)是true了,setState做的就是將更新都統(tǒng)一push到dirtyComponents數(shù)組中;

4、在事務結束的時候才通過 ReactUpdates.flushBatchedUpdates 方法將所有的臨時 state merge 并計算出最新的 props 及 state,然后將批量執(zhí)行關閉結束事務。

到這里我并沒有順著ReactUpdates.flushBatchedUpdates方法講下去,這部分涉及到渲染和Virtual Dom的內(nèi)容,反正你知道它是拿來執(zhí)行渲染的就行了。

到這里為止,setState的核心概念已經(jīng)比較清楚了,再往下的內(nèi)容,暫時先知道就行了,不然展開來講一環(huán)扣一環(huán)太雜了,我們做事情要把握核心。

到這里不知道有沒有同學想起一個問題

isBatchingUpdates 標志位在 batchedUpdates 發(fā)起的時候被置為 true ,那什么時候被復位為false的呢?

還記得上面的事務的close方法嗎,同一個文件src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

var transaction = new ReactDefaultBatchingStrategyTransaction();
// 定義復位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定義批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

相信眼尖的同學已經(jīng)看到了,close的時候復位,把isBatchingUpdates設置為false。

image.png
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

通過原型合并,事務的close 方法,將在 enqueueUpdate 執(zhí)行結束后,先把 isBatchingUpdates 復位,再發(fā)起一個 DOM 的批更新

到這里,我們會發(fā)現(xiàn),前面所有的隊列、batchUpdate等等都是為了來到事務的這一步,前面都只是批收集的工作,到這里才真正的完成了批更新的操作。

當然在實際代碼中 React 還做了異常處理等工作,這里不詳細展開。有興趣的同學可以參考源碼中 Transaction 實現(xiàn)。

說了這么多 Transaction,關于上文提到的RESET_BATCHED_UPDATES主要用來管理isBatchingUpdates狀態(tài)這句話是不是;理解更透徹了吶?

上文提到了兩個wrapper:RESET_BATCHED_UPDATES和FLUSH_BATCHED_UPDATES。RESET_BATCHED_UPDATES用來管理isBatchingUpdates狀態(tài),我們前面在分析setState是否立即生效時已經(jīng)講解過了。那FLUSH_BATCHED_UPDATES用來干嘛呢?

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var flushBatchedUpdates = function () {
  // 循環(huán)遍歷處理完所有dirtyComponents
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // close前執(zhí)行完runBatchedUpdates方法,這是關鍵
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

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

FLUSH_BATCHED_UPDATES會在一個transaction的close階段運行runBatchedUpdates,從而執(zhí)行update。

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

  for (var i = 0; i < len; i++) {
    // dirtyComponents中取出一個component
    var component = dirtyComponents[i];

    // 取出dirtyComponent中的未執(zhí)行的callback,下面就準備執(zhí)行它了
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      if (component._currentElement.props === component._renderedComponent._currentElement) {
        namedComponent = component._renderedComponent;
      }
    }
    // 執(zhí)行updateComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    // 執(zhí)行dirtyComponent中之前未執(zhí)行的callback
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }
}

runBatchedUpdates循環(huán)遍歷dirtyComponents數(shù)組,主要干兩件事。首先執(zhí)行performUpdateIfNecessary來刷新組件的view,然后執(zhí)行之前阻塞的callback。下面來看performUpdateIfNecessary。

performUpdateIfNecessary: function (transaction) {
  if (this._pendingElement != null) {
    // receiveComponent會最終調(diào)用到updateComponent,從而刷新View
    ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
  }

  if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
    // 執(zhí)行updateComponent,從而刷新View。這個流程在React生命周期中講解過
    this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
  }
}

最后驚喜的看到了receiveComponent和updateComponent吧。receiveComponent最后會調(diào)用updateComponent,而updateComponent中會執(zhí)行React組件存在期的生命周期方法,如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。 從而完成組件更新的整套流程。

updateComponent: function(
    transaction,
    prevParentElement,
    nextParentElement,
    prevUnmaskedContext,
    nextUnmaskedContext,
  ) {
    var inst = this._instance;
    invariant(
      inst != null,
      'Attempted to update component `%s` that has already been unmounted ' +
        '(or failed to mount).',
      this.getName() || 'ReactCompositeComponent',
    );

    var willReceive = false;
    var nextContext;

    // Determine if the context has changed or not
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    var prevProps = prevParentElement.props;
    var nextProps = nextParentElement.props;

    // Not a simple state update but a props update
    if (prevParentElement !== nextParentElement) {
      willReceive = true;
    }

    // An update here will schedule an update but immediately set
    // _pendingStateQueue which will ensure that any state updates gets
    // immediately reconciled instead of waiting for the next batch.
    if (willReceive && inst.componentWillReceiveProps) {
        inst.componentWillReceiveProps(nextProps, nextContext);
    }

    var nextState = this._processPendingState(nextProps, nextContext);
    var shouldUpdate = true;

    if (!this._pendingForceUpdate) {
      if (inst.shouldComponentUpdate) {
         shouldUpdate = inst.shouldComponentUpdate(
            nextProps,
            nextState,
            nextContext,
          );
      } else {
        if (this._compositeType === CompositeTypes.PureClass) {
          shouldUpdate =
            !shallowEqual(prevProps, nextProps) ||
            !shallowEqual(inst.state, nextState);
        }
      }
    }
    this._updateBatchNumber = null;
    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;
    }
  },

  _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 = Object.assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      Object.assign(
        nextState,
        typeof partial === 'function'
          ? partial.call(inst, nextState, props, context)
          : partial,
      );
    }

    return nextState;
  },

這一部分代碼相對來說不算是很難,replace是存在是由于之前被廢棄的APIthis.replaceState,我們現(xiàn)在不需要關心這一部分,現(xiàn)在我們可以回答剛開始的問題,為什么給setState傳入的參數(shù)是函數(shù)時,就可以解決剛開始的例子。

Object.assign(
    nextState,
    typeof partial === 'function' ?
        partial.call(inst, nextState, props, context) :
        partial
);

如果我們傳入的是對象

this.setState({value: this.state.value + 1 });
this.setState({value: this.state.value + 1})

我們現(xiàn)在已經(jīng)知道,調(diào)用setState是批量更新,那么第一次調(diào)用之后,this.state.value的值并沒有改變。兩次更新的value值其實是一樣的,所以達不到我們的目的。但是如果我們傳遞的是回調(diào)函數(shù)的形式,那么情況就不一樣了,partial.call(inst, nextState, props, context)接受的state都是上一輪更新之后的新值,因此可以達到我們預期的目的?!?br>    
_processPendingState在計算完新的state之后,會_performComponentUpdate:

function _performComponentUpdate(
    nextElement,
    nextProps,
    nextState,
    nextContext,
    transaction,
    unmaskedContext
  ) {
    var inst = this._instance;

    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    this._updateRenderedComponent(transaction, unmaskedContext);

    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
}

我們可以看到,這部分內(nèi)容涉及到了幾方面內(nèi)容,首先在更新前調(diào)用了鉤子函數(shù)componentWillUpdate,然后更新了組件的屬性(props、state、context),執(zhí)行函數(shù)_updateRenderedComponent,最后再次執(zhí)行鉤子函數(shù)componentDidUpdate。

_updateRenderedComponent執(zhí)行組件的render方法。
在文件/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js中,代碼如下:

 /**
   * Call the component's `render` method and update the DOM accordingly.
   *
   * @param {ReactReconcileTransaction} transaction
   * @internal
   */
  _updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;
    var nextRenderedElement = this._renderValidatedComponent();

    var debugID = 0;

    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      ReactReconciler.receiveComponent(
        prevComponentInstance,
        nextRenderedElement,
        transaction,
        this._processChildContext(context),
      );
    } else {
      var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      var nodeType = ReactNodeTypes.getType(nextRenderedElement);
      this._renderedNodeType = nodeType;
      var child = this._instantiateReactComponent(
        nextRenderedElement,
        nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
      );
      this._renderedComponent = child;

      var nextMarkup = ReactReconciler.mountComponent(
        child,
        transaction,
        this._hostParent,
        this._hostContainerInfo,
        this._processChildContext(context),
        debugID,
      );
      this._replaceNodeWithMarkup(
        oldHostNode,
        nextMarkup,
        prevComponentInstance,
      );
    }
  },

  /**
   * Overridden in shallow rendering.
   *
   * @protected
   */
  _replaceNodeWithMarkup: function(oldHostNode, nextMarkup, prevInstance) {
    ReactComponentEnvironment.replaceNodeWithMarkup(
      oldHostNode,
      nextMarkup,
      prevInstance,
    );
  },

  /**
   * @protected
   */
  _renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    var renderedElement;

    renderedElement = inst.render();
    return renderedElement;
  },

到目前為止,我們已經(jīng)基本介紹完了setState的更新過程,只剩一個部分沒有介紹,那就是setState執(zhí)行結束之后的回調(diào)函數(shù)。我們知道,setState函數(shù)中如果存在callback,則會有:

if (callback) {
    this.updater.enqueueCallback(this, callback);
}

call函數(shù)會被傳遞給this.updater的函數(shù)enqueueCallback,然后非常類似于setState,callback會存儲在組件內(nèi)部實例中的_pendingCallbacks屬性之中。我們知道,回調(diào)函數(shù)必須要setState真正完成之后才會調(diào)用,那么在代碼中是怎么實現(xiàn)的。大家還記得在函數(shù)flushBatchedUpdates中有一個事務ReactUpdatesFlushTransaction:

//代碼有省略
var flushBatchedUpdates = function() {
  while (dirtyComponents.length) {
    if (dirtyComponents.length) {
      //從事務pool中獲得事務實例
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      //釋放實例
      ReactUpdatesFlushTransaction.release(transaction);
    }
    //......
  }
};

我們現(xiàn)在看看ReactUpdatesFlushTransaction的wrapper是怎么定義的:

var UPDATE_QUEUEING = {
  initialize: function() {
    this.callbackQueue.reset();
  },
  close: function() {
    this.callbackQueue.notifyAll();
  },
};

我們看到在事務的close階段定義了this.callbackQueue.notifyAll(),即執(zhí)行了回調(diào)函數(shù),通過這種方法就能保證回調(diào)函數(shù)一定是在setState真正完成之后才執(zhí)行的。到此為止我們基本已經(jīng)解釋了setState大致的流程是怎樣的,但是我們還是沒有回答之前的一個問題,為什么下面的兩種代碼會產(chǎn)生不同的情況:

//未按預期執(zhí)行
_addValue() {
    this.setState({
        value: this.state.value + 1
    })
    this.setState({
        value: this.state.value + 1
    })
}
//按預期執(zhí)行
_addValue() {
    setTimeout(()=>{
        this.setState({
            value: this.state.value + 1
        });
        this.setState({
            value: this.state.value + 1
        });
    },0)
}

這個問題,其實真的要追本溯源地去講,是比較復雜的,我們簡要介紹一下。在第一種情況下,如果打斷點追蹤你會發(fā)現(xiàn),在第一次執(zhí)行setState前,已經(jīng)觸發(fā)了一個 batchedUpdates,等到執(zhí)行setState時已經(jīng)處于一個較大的事務,因此兩個setState都是會被批量更新的(相當于異步更新的過程,thi.state.value值并沒有立即改變),執(zhí)行setState只不過是將兩者的partialState傳入dirtyComponents,最后再通過事務的close階段的flushBatchedUpdates方法去執(zhí)行重新渲染。但是通過setTimeout函數(shù)的包裝,兩次setState都會在click觸發(fā)的批量更新batchedUpdates結束之后執(zhí)行,這兩次setState會觸發(fā)兩次批量更新batchedUpdates,當然也會執(zhí)行兩個事務以及函數(shù)flushBatchedUpdates,這就相當于一個同步更新的過程,自然可以達到我們的目的,這也就解釋了為什么React文檔中既沒有說setState是同步更新或者是異步更新,只是模糊地說到,setState并不保證同步更新。

舉個??

u=4000503827,2702628120&fm=26&gp=0.jpg

如下代碼:

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

// 結果就為 0, 0, 2, 3
  • setState 只在合成事件和鉤子函數(shù)中是“異步”的,在原生事件和setTimeout 中都是同步的。
  • setState 的“異步”并不是說內(nèi)部由異步代碼實現(xiàn),其實本身執(zhí)行的過程和代碼都是同步的,只是合成事件和- 鉤子函數(shù)的調(diào)用順序在更新之前,導致在合成事件和鉤子函數(shù)中沒法立馬拿到更新后的值,形成了所謂的“異步”,當然可以通過第二個參數(shù) setState(partialState, callback) 中的callback拿到更新后的結果。
  • setState的批量更新優(yōu)化也是建立在“異步”(合成事件、鉤子函數(shù))之上的,在原生事件和setTimeout 中不會批量更新,在“異步”中如果對同一個值進行多次setState,setState的批量更新策略會對其進行覆蓋,取最后一次的執(zhí)行,如果是同時setState多個不同的值,在更新時會對其進行合并批量更新。
    所以基于上述結論,如果想要實現(xiàn)上述代碼中 4 次 console.log 打印出來的 val 分別是1、2、3、4。可以實現(xiàn)如下:
setTimeout(() => {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 1

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 2

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 3

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 4
}, 0);

或者

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 1

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 2

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 3

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 4

setState 干了什么

1、合成事件中的setState

react為了解決跨平臺,兼容性問題,自己封裝了一套事件機制,代理了原生的事件,像在jsx中常見的onClick、onChange這些都是合成事件。
在react的生命周期和合成事件中,react仍然處于他的更新機制中,這時isBranchUpdate為true。
按照上述過程,這時無論調(diào)用多少次setState,都會不會執(zhí)行更新,而是將要更新的state存入_pendingStateQueue,將要更新的組件存入dirtyComponent。
當上一次更新機制執(zhí)行完畢,以生命周期為例,所有組件,即最頂層組件didmount后會將isBranchUpdate設置為false。這時將執(zhí)行之前累積的setState。

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新前的val --> 0
  }

  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

2、生命周期函數(shù)中的setState

整個生命周期中就是一個事物操作,所以標識位isBatchingUpdates = true,所以流程到了enqueueUpdate()時,實例對象都會加入到dirtyComponents 數(shù)組中

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 輸出的還是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

3、原生事件中的setState

原生事件是指非react合成事件,原生自帶的事件監(jiān)聽 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 這種綁定事件的形式都屬于原生事件
原生事件綁定不會通過合成事件的方式處理,自然也不會進入更新事務的處理流程。setTimeout也一樣,在setTimeout回調(diào)執(zhí)行時已經(jīng)完成了原更新組件流程,不會放入dirtyComponent進行異步更新,其結果自然是同步的。

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 輸出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

4、setTimeout中的setState

由執(zhí)行機制看,setState本身并不是異步的,而是如果在調(diào)用setState時,如果react正處于更新過程,當前更新會被暫存,等上一次更新執(zhí)行后在執(zhí)行,這個過程給人一種異步的假象。

在生命周期,根據(jù)event loop的模型,會將異步函數(shù)先暫存,等所有同步代碼執(zhí)行完畢后在執(zhí)行,這時上一次更新過程已經(jīng)執(zhí)行完畢,isBranchUpdate被設置為false,根據(jù)上面的流程,這時再調(diào)用setState即可立即執(zhí)行更新,拿到更新結果。

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 輸出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

5、批量更新

在 setState 的時候react內(nèi)部會創(chuàng)建一個 updateQueue ,通過 firstUpdate 、 lastUpdate 、 lastUpdate.next 去維護一個更新的隊列,在最終的 performWork 中,相同的key會被覆蓋,只會對最后一次的 setState 進行更新
分別執(zhí)行以下代碼:

  componentDidMount() {
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
    this.setState({ index: this.state.index + 1 }, () => {
      console.log(this.state.index);
    })
  }
  componentDidMount() {
    this.setState((preState) => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
    this.setState(preState => ({ index: preState.index + 1 }), () => {
      console.log(this.state.index);
    })
  }

執(zhí)行結果:

1
1
2
2

說明:

1.直接傳遞對象的setstate會被合并成一次
2.使用函數(shù)傳遞state不會被合并

批量更新中State合并機制

我們看下流程中_processPendingState的代碼,這個函數(shù)是用來合并state暫存隊列的,最后返回一個合并后的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;
  },

我們只需要關注下面這段代碼:

_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);

如果傳入的是對象,很明顯會被合并成一次:

Object.assign(
  nextState,
  {index: state.index+ 1},
  {index: state.index+ 1}
)

如果傳入的是函數(shù),函數(shù)的參數(shù)preState是前一次合并后的結果,所以計算結果是準確的。

總結

setState流程還是很復雜的,設計也很精巧,避免了重復無謂的刷新組件。它的主要流程如下:

  1. enqueueSetState將state放入隊列中,并調(diào)用enqueueUpdate處理要更新的Component;

2.如果組件當前正處于update事務中,則先將Component存入dirtyComponent中。否則調(diào)用batchedUpdates處理。

3.batchedUpdates發(fā)起一次transaction.perform()事務;

4.開始執(zhí)行事務初始化,運行,結束三個階段;

初始化:事務初始化階段沒有注冊方法,故無方法要執(zhí)行;
運行:執(zhí)行setSate時傳入的callback方法,一般不會傳callback參數(shù);
結束:更新isBatchingUpdates為false,并執(zhí)行FLUSH_BATCHED_UPDATES這個wrapper中的close方法。
5.FLUSH_BATCHED_UPDATES在close階段,會循環(huán)遍歷所有的dirtyComponents,調(diào)用updateComponent刷新組件,并執(zhí)行它的pendingCallbacks, 也就是setState中設置的callback。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容