1. 程式人生 > >React原始碼分析(二)-元件的初始渲染

React原始碼分析(二)-元件的初始渲染

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

正文

在React 原始碼中, 首次渲染元件有一個重要的過程, mount, 插入, 即插入到DOM中, 發生在例項化元件之後. 這是一個不斷生成(render)不斷插入、類似遞迴的過程. 讓我們一步一步來分析.

使用事務執行插入過程

我們來看首先在插入之前的準備, ReactMount.js中, batchedMountComponentIntoNode

被放到了批量策略batchedUpdates中執行, batchedMountComponentIntoNode 函式正是執行插入過程的第一步

1
2
3
4
5
6
// 放在批量策略batchedUpdates中執行插入
ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    ...
);

這個batchingStrategy就是ReactDefaultBatchingStrategy, 因此呼叫了ReactDefaultBatchingStrategy

batchedUpdates, 並將batchedMountComponentIntoNode當作callback.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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,
    ...
  );
    ...
}

相信你注意到了 ReactUpdates.ReactReconcileTransaction.getPooled, 這個函式的作用就是從物件池裡拿到ReactReconcileTransaction 物件重用.

React優化策略——物件池

在ReactMount.js :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 新增物件池, 實質就是對類包裝
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方法:

ReactDOMComponent型別:

ReactDOMTextComponent型別:

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

剛開始, 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. 在以上過程中, 依靠事務進行儲存更新、回撥佇列, 在事務結束時批量更新.