窺探React-原始碼分析(二)

莫凡_Tcg發表於2019-02-28

上一篇文章講到了React 呼叫ReactDOM.render首次渲染元件的前幾個過程的原始碼, 包括建立元素、根據元素例項化對應元件, 利用事務來進行批量更新. 我們還穿插介紹了React 事務的實現以及如何利用事務進行批量更新的實現. 這篇文章我們接著分析後面的過程, 包括呼叫了哪些事務, 元件插入的過程, 元件生命週期方法什麼時候被呼叫等.

正文

在React 原始碼中, 首次渲染元件有一個重要的過程, mount, 插入, 即插入到DOM中, 發生在例項化元件之後. React使用批量策略來管理元件插入到DOM的過程. 這個“批量”不是指像遍歷陣列那樣同批次插入, 而是一個不斷生成不斷插入、類似遞迴的過程. 讓我們一步一步來分析.

使用批量策略管理插入

如何管理呢? 即在插入之前就開始一次batch, 然後插入過程中任何更新都會被enqueue, 在batchingStrategy事務的close階段批量更新.

啟動策略

我們來看首先在插入之前的準備, ReactMount.js中, batchedMountComponentIntoNode被放到了批量策略batchedUpdates中執行 :

// 放在批量策略batchedUpdates中執行插入
ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    ...
);
複製程式碼

從上篇文章展示的原始碼中看到, 這個batchingStrategy就是ReactDefaultBatchingStrategy, 因此呼叫了ReactDefaultBatchingStrategybatchedUpdates, 並將batchedMountComponentIntoNode當作callback.

執行策略

繼續看ReactDefaultBatchingStrategybatchedUpdates, 在ReactDefaultBatchingStrategy.js :

// 批處理策略
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, // 是否處在一次BatchingUpdates標誌位

  // 批量更新策略呼叫的就是這個方法
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
	// 一旦呼叫批處理, 重置isBatchingUpdates標誌位, 表示正處在一次BatchingUpdates中
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // 首次插入時, 由於是第一次啟動批量策略, 因此alreadyBatchingUpdates為false, 執行事務
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);  // 將callback放進事務裡執行
    }
  },
};
複製程式碼

在執行插入的過程中enqueue更新

我們在componentWillMount裡setState, 看看React會怎麼做:

// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, `setState`);
  }
};

//ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
	// enqueueUpdate
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
 }

//ReactUpdate.js:
function enqueueUpdate(component) {
  ensureInjected(); // 注入預設策略
    
    // 如果不是在一次batch就開啟一次batch
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
    // 如果是就儲存更新
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
複製程式碼

批量更新

在ReactUpdates.js中

var flushBatchedUpdates = function () {
  // 批量處理dirtyComponents
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
// 批量處理callback
    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};
複製程式碼

使用事務執行插入過程

batchedUpdates啟動一個策略事務去執行batchedMountComponentIntoNode, 以便利用策略控制更新, 而在這個函式中又啟動了一個調和(Reconcile)事務, 以便管理插入.

// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction();
...
var ReactDefaultBatchingStrategy = {
  ...
  batchedUpdates: function(callback, a, b, c, d, e) {
   ...
    // 啟動ReactDefaultBatchingStrategy事務
      return transaction.perform(callback, null, a, b, c, d, e);
  },
};

// ReactMount.js
function batchedMountComponentIntoNode(
  ...
) {
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
  );
    // 啟動Reconcile事務
  transaction.perform(
    mountComponentIntoNode,
    ...
  );
    ...
}

複製程式碼

React優化策略——物件池

在ReactMount.js :

function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {
    // 從物件池中拿到ReactReconcileTransaction事務
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
  );
    // 啟動事務執行mountComponentIntoNode
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
    // 釋放事務
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}
複製程式碼

React 在啟動另一個事務之前拿到了這個事務, 從哪裡拿到的呢? 這裡就涉及到了React 優化策略之一——物件池

GC很慢

首先你用JavaScript宣告的變數不再使用時, js引擎會在某些時間回收它們, 這個回收時間是耗時的. 資料顯示:

Marking latency depends on the number of live objects that have to be marked, with marking of the whole heap potentially taking more than 100 ms for large webpages.

整個堆的標記對於大型網頁很可能需要超過100毫秒

儘管V8引擎對垃圾回收有優化, 但為了避免重複建立臨時物件造成GC不斷啟動以及複用物件, React使用了物件池來複用物件, 對GC表明, 我一直在使用它們, 請不要啟動回收.

React 實現的物件池其實就是對類進行了包裝, 給類新增一個例項佇列, 用時取, 不用時再放回, 防止重複例項化:

PooledClass.js :

// 新增物件池, 實質就是對類包裝
var addPoolingTo = function (CopyConstructor, pooler) {
  // 拿到類
  var NewKlass = CopyConstructor;
  // 新增例項佇列屬性
  NewKlass.instancePool = [];
  // 新增拿到例項方法
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  // 例項佇列預設為10個
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  // 將例項放回佇列
  NewKlass.release = standardReleaser;
  return NewKlass;
};
// 從物件池申請一個例項.對於不同引數數量的類,React分別處理, 這裡是一個引數的類的申請例項的方法, 其他一樣
var oneArgumentPooler = function(copyFieldsFrom) {
  // this 指的就是傳進來的類
  var Klass = this;
  // 如果類的例項佇列有例項, 則拿出來一個
  if (Klass.instancePool.length) {
    var instance = Klass.instancePool.pop();
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else { // 否則說明是第一次例項化, new 一個
    return new Klass(copyFieldsFrom);
  }
};
// 釋放例項到類的佇列中
var standardReleaser = function(instance) {
  var Klass = this;
  ...
  // 呼叫類的解構函式
  instance.destructor();
  // 放到佇列
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};

// 使用時將類傳進去即可
PooledClass.addPoolingTo(ReactReconcileTransaction);
複製程式碼

可以看到, React物件池就是給類維護一個例項佇列, 用到就pop一個, 不用就push回去. 在React原始碼中, 用完例項後要立即釋放, 也就是申請和釋放成對出現, 達到優化效能的目的.

插入過程

在ReactMount.js中, mountComponentIntoNode函式執行了元件例項的mountComponent, 不同的元件例項有自己的mountComponent方法, 做的也是不同的事情. (原始碼我就不上了, 太TM…)

ReactCompositeComponent型別的mountComponent方法:

窺探React-原始碼分析(二)

ReactDOMComponent型別:

窺探React-原始碼分析(二)

ReactDOMTextComponent型別:

窺探React-原始碼分析(二)

整個mount過程是遞迴渲染的(向量圖):

窺探React-原始碼分析(二)

剛開始, React給要渲染的元件從最頂層加了一個ReactCompositeComponent型別的 topLevelWrapper來方便的儲存所有更新, 因此初次遞迴是從 ReactCompositeComponent 的mountComponent 開始的, 這個過程會呼叫元件的render函式(如果有的話), 根據render出來的elements再呼叫instantiateReactComponent例項化不同型別的元件, 再呼叫元件的 mountComponent, 因此這是一個不斷渲染不斷插入、遞迴的過程.

總結

React 初始渲染主要分為以下幾個步驟:

  1. 構建一個元件的elements tree(subtree)—— 從元件巢狀的最裡層(轉換JSX後最裡層的createElements函式)開始層層呼叫createElements建立這個元件elements tree. 在這個subtree中, 裡層建立出來的元素作為包裹層的props.children;
  2. 例項化元件——根據當前元素的型別建立對應型別的元件例項;
  3. 利用多種事務執行元件例項的mountComponent.
    1. 首先執行topLevelWrapper(ReactCompositeComponent)的mountComponent;
    2. ReactCompositeComponent的mountComponent過程中會先呼叫render(Composite型別 )生成元件的elements tree, 然後順著props.children, 不斷例項化, 不斷呼叫各自元件的mountComponent 形成迴圈
  4. 在以上過程中, 依靠事務進行儲存更新、回撥佇列, 在事務結束時批量更新.

相關文章