關於React setState的實現原理(二)


React中的Transaction

大家學過sql server的都知道我們可以批量處理sql語句,原理其實都是基於上一篇我們說的Datch Update機制。當所有的操作均執行成功,才會執行修改操作;若有一個操作失敗,則執行rollback(回滾)。

在React中,我們介紹過事件會在函數前后執行自己的邏輯,具體就是調用perform方法進入一個事件,這個方法會傳入一個method參數。執行perform時先執行initializeAll方法按照一定順序執行一系列的initialize

慚怍,然后執行傳入的method,method執行完后,就執行closeAll方法按照一定順序執行一系列的close操作。注意一種事件不能同時開啟,否則會拋出異常。給一個例子是實現事件:

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

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

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

 

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

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

 

組件調用ReactDOM.render()之后,會執行一個_renderNewRootComponent的方法,大概是該方法執行了一個ReactUpdates.batchedUpdates()。 那么batchedUpdates是什么呢?

var transaction = new ReactDefaultBatchingStrategyTransaction();

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren't updated unnecessarily.
   */
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  }
};

 

從代碼中可以看出,這個batchedUpdate是第一次調用alreadyBatchingUpdates是false(儲存起來了),回去執行transaction.perform(method)(前邊說過perform執行會進入一個時間),這樣就進入第一個事務,

這個事務是啥我們現在不用管,我們只需要知道這個transaction是ReactDefaultBatchingStrategyTransaction的實例,它代表了其中一類事務的執行。然后會執行method方法,就會進行組件的首次裝載。完成后會調用

componentDidMount(注意,此時還是在執行method方法,事務還沒結束,事務只有在執行完method后執行一系列close才會結束),在該方法中,我們調用了setState,出現了一系列奇怪的現象。因此,我們再來看看

setState方法:

ReactComponent.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

setState在調用時做了兩件事,第一,調用enqueueSetState。該方法將我們傳入的partialState添加到一個叫做_pendingStateQueue的隊列中去存起來,然后執行一個enqueueUpdate方法。第二,如果存在callback就調用enqueueCallback將其存入一個_pendingCallbacks隊列中存起來。然后我們來看enqueueUpdate方法。

function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

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

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

 

上面的代碼中,個batchingStrategy就是上面的ReactDefaultBatchingStrategy,只是它通過inject的形式對其進行賦值,比較隱蔽。因此,我們當前的setState已經處於了這一類事務之中,isBatchingUpdates已經被置為true,所以將會把它添加到dirtyComponents中,在某一時刻做批量更新。因此在前兩個setState中,並沒有做任何狀態更新,以及組件更新的事,而僅僅是將新的state和該組件存在了隊列之中,因此兩次都會打印出0,我們之前的第一個問題就解決了,還有一個問題,我們接着往下走。

在setTimeout中執行的setState打印出了2和3,有了前面的鋪墊,我們大概就能得出結論,這應該就是因為這兩次setState分別執行了一次完整的事務,導致state被直接更新而造成的結果。那么問題來了,為什么setTimeout中的setState會分別執行兩次不同的事務?之前執行ReactDOM.render開啟的事務在什么時候結束了?我們來看下列代碼。

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

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;
  }
});

這段代碼也是寫在ReactDefaultBatchingStrategy這個對象中的。我們之前提到這個事務中transaction是ReactDefaultBatchingStrategyTransaction的實例,這段代碼其實就是給該事務添加了兩個在事務結束時會被調用的close方法。即在perform中的method執行完畢后,會按照這里數組的順序[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]依次調用其close方法。FLUSH_BATCHED_UPDATES是執行批量更新操作。RESET_BATCHED_UPDATES我們可以看到將isBatchingUpdates變回false,即意味着事務結束。

function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  if (!batchingStrategy.isBatchingUpdates) { //上一個事件結束執行過isBatchedUpdates=false,所以進入if中
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

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

 


接下來再調用setState時(在setTimeout中,前文說過一步操作不會在主線程,我理解是在主線程結束才會執行,此時的主線程事件已經結束),enqueueUpdate不會再將其添加到dirtyComponents中,而是執行batchingStrategy.batchedUpdates(enqueueUpdate, component)開啟一個新事務。

但是需要注意,這里傳入的參數是enqueueUpdate,即perform中執行的method為enqueueUpdate,而再次調用該enqueueUpdate方法會去執行dirtyComponents那一步。這就可以理解為,處於單獨事務的setState也是通過將組件添加到dirtyComponents來完成更新的,只不過這里是在enqueueUpdate執行完畢后立即執行相應的close方法完成更新,而前面兩個setState需在整個組件裝載完成之后,即在componentDidMount執行完畢后才會去調用close完成更新。總結一下4個setState執行的過程就是:先執行兩次console.log,然后執行批量更新,再執行setState直接更新,執行console.log,最后再執行setState直接更新,再執行console.log,所以就會得出0,0,2,3。

到現在上面的問題已經解決,但是又出現一個新問題:

class Root extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState({
      count: me.state.count + 1
    });
    me.setState({
      count: me.state.count + 1
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>   //頁面中將打印出1
    )
  }
}
class Root extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let me = this;
    me.setState(function(state, props) {
      return {
        count: state.count + 1
      }
    });
    me.setState(function(state, props) {
      return {
        count: state.count + 1
      }
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>   //頁面中將打印出2
    )
  }
}

 

這兩種寫法,一個是在setState中傳入了object,一個是傳入了function,卻得到了兩種不同的結果,這是什么原因造成的,這就需要我們去深入了解一下進行批量更行時都做了些什么。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM