React 源碼學習(四):事務機制

閱讀源碼成了今年的學習目標之一,在選擇 Vue 和 React 之間,我想先閱讀 React 。 在考慮到讀哪一個版本的時候,我想先接觸到源碼早期的思想可能會更輕鬆一些,最終我選擇閱讀 0.3-stable 。 那麼接下來,我將從幾個方面來解讀這個版本的源碼。javascript

  1. React 源碼學習(一):HTML 元素渲染
  2. React 源碼學習(二):HTML 子元素渲染
  3. React 源碼學習(三):CSS 樣式及 DOM 屬性
  4. React 源碼學習(四):事務機制
  5. React 源碼學習(五):事件機制
  6. React 源碼學習(六):組件渲染
  7. React 源碼學習(七):生命週期
  8. React 源碼學習(八):組件更新

什麼是事務

咱們須要直接看到事務的整個概念:html

英文版請看源碼,如下爲 Google 翻譯內容java

Transaction 建立一個黑盒子,它可以包裝任何方法,以便在調用方法以前和以後維護某些不變量(即便在調用包裝方法時拋出異常)。 實例化事務的人能夠在建立時提供不變量的執行者。 Transaction 類自己將爲您提供一個額外的自動不變量 - 任何事務實例在運行時不該該運行的不變量。您一般會建立一個 Transaction 的單個實例,以便屢次重用,這可能用於包裝多個不一樣的方法。包裝器很是簡單 - 它們只須要實現兩種方法。node

/** * <pre> * wrappers (建立時注入) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(任何方法) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->| 任何方法 |---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre> */
複製代碼

獎金:react

  • 按方法名稱和包裝器索引報告時間度量標準。

用例:算法

  • 在對賬以前/以後保留輸入選擇範圍。即便出現意外錯誤也能夠恢復選擇。
  • 在從新排列DOM時停用事件,防止模糊/焦點,同時保證過後系統從新激活。
  • 在工做線程中進行協調後,將收集的DOM突變隊列刷新到主UI線程。
  • 在呈現新內容後調用任何收集的 componentDidRender 回調。
  • (將來用例):包裝 ReactWorker 隊列的特定刷新以保留 scrollTop (自動滾動感知DOM)。
  • (將來用例):DOM更新以前和以後的佈局計算。

事務性插件 API:數組

  • 具備返回任何預計算的 initialize 方法的模塊。
  • 和一個接受預計算的 close 方法。當包裝過程完成或失敗時,調用 close

事務 Transaction

// utils/Transaction.js
var Mixin = {
  /** * 設置此實例,以便爲收集指標作好準備。 這樣作是爲了使這個設置方法能夠在已經初始化的實例上使用, * 其方式是在重用時不消耗額外的內存。 若是您決定將此 mixin 的子類化爲 "PooledClass" ,那麼這將很是有用。 */
  reinitializeTransaction: function() {
    // this.getTransactionWrappers() 用於獲取上面概念中的 wrappers
    // this.transactionWrappers 就是 wrappers
    this.transactionWrappers = this.getTransactionWrappers();
    // 初始化用於存儲 initialize 時返回的內容的 this.wrapperInitData
    if (!this.wrapperInitData) {
      this.wrapperInitData = [];
    } else {
      this.wrapperInitData.length = 0;
    }
    // 時間方面,這裏不作解讀
    if (!this.timingMetrics) {
      this.timingMetrics = {};
    }
    this.timingMetrics.methodInvocationTime = 0;
    if (!this.timingMetrics.wrapperInitTimes) {
      this.timingMetrics.wrapperInitTimes = [];
    } else {
      this.timingMetrics.wrapperInitTimes.length = 0;
    }
    if (!this.timingMetrics.wrapperCloseTimes) {
      this.timingMetrics.wrapperCloseTimes = [];
    } else {
      this.timingMetrics.wrapperCloseTimes.length = 0;
    }
    // 初始化事務標記
    this._isInTransaction = false;
  },
  _isInTransaction: false,
  getTransactionWrappers: null,
  isInTransaction: function() {
    return !!this._isInTransaction;
  },
  // 在安全窗口內執行該功能。 將此用於頂級方法,這些方法會致使須要進行安全檢查的大量計算/突變。
  perform: function(method, scope, a, b, c, d, e, f) {
    throwIf(this.isInTransaction(), DUAL_TRANSACTION);
    var memberStart = Date.now();
    // 報錯存儲
    var err = null;
    // method 返回內容
    var ret;
    try {
      // 開始 initialize
      this.initializeAll();
      // initialize 完成後調用 method
      ret = method.call(scope, a, b, c, d, e, f);
    } catch (ie_requires_catch) {
      // 抓報錯,這裏報錯會被覆蓋
      err = ie_requires_catch;
    } finally {
      var memberEnd = Date.now();
      this.methodInvocationTime += (memberEnd - memberStart);
      try {
        // 開始 close
        this.closeAll();
      } catch (closeAllErr) {
        // 抓報錯,這裏若前面存在報錯,則取前面的報錯
        err = err || closeAllErr;
      }
    }
    // 拋出報錯
    if (err) {
      throw err;
    }
    // 返回 method 執行結果
    return ret;
  },
  initializeAll: function() {
    // 事務開始
    this._isInTransaction = true;
    var transactionWrappers = this.transactionWrappers;
    var wrapperInitTimes = this.timingMetrics.wrapperInitTimes;
    var err = null;
    // 遍歷 wrappers
    for (var i = 0; i < transactionWrappers.length; i++) {
      var initStart = Date.now();
      var wrapper = transactionWrappers[i];
      try {
        // 執行 initialize ,並把返回值存入對應的 this.wrapperInitData[i]
        this.wrapperInitData[i] =
          wrapper.initialize ? wrapper.initialize.call(this) : null;
      } catch (initErr) {
        err = err || initErr;  // Remember the first error.
        // 有報錯的話對應位置存入 Transaction.OBSERVED_ERROR
        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
      } finally {
        var curInitTime = wrapperInitTimes[i];
        var initEnd = Date.now();
        wrapperInitTimes[i] = (curInitTime || 0) + (initEnd - initStart);
      }
    }
    // 拋出錯誤
    if (err) {
      throw err;
    }
  },
  /** * 調用 `this.transactionWrappers.close[i]` 函數中的每個,向它們傳遞 * `this.transactionWrappers.init[i]` 的相應返回值(對應於失敗的初始值設定項的 `close`rs 將不被調用)。 */
  closeAll: function() {
    throwIf(!this.isInTransaction(), MISSING_TRANSACTION);
    var transactionWrappers = this.transactionWrappers;
    var wrapperCloseTimes = this.timingMetrics.wrapperCloseTimes;
    var err = null;
    for (var i = 0; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var closeStart = Date.now();
      // 獲取 initialize 時對應的返回值
      var initData = this.wrapperInitData[i];
      try {
        // 返回值不等於 Transaction.OBSERVED_ERROR 時才執行 close
        if (initData !== Transaction.OBSERVED_ERROR) {
          wrapper.close && wrapper.close.call(this, initData);
        }
      } catch (closeErr) {
        err = err || closeErr;  // Remember the first error.
      } finally {
        var closeEnd = Date.now();
        var curCloseTime = wrapperCloseTimes[i];
        wrapperCloseTimes[i] = (curCloseTime || 0) + (closeEnd - closeStart);
      }
    }
    // 釋放內存
    this.wrapperInitData.length = 0;
    // 事務關閉
    this._isInTransaction = false;
    // 拋出錯誤
    if (err) {
      throw err;
    }
  }
};

var Transaction = {
  Mixin: Mixin,
  OBSERVED_ERROR: {}
};
複製代碼

React 事務

上面這段代碼看完,我相信你對事務的整個概念已經有所瞭解,那麼事務到底在處理什麼的時候用到了呢?接下來就要揭曉本次一樣重點的內容 React 調度事務, ReactReconcileTransaction瀏覽器

// core/ReactReconcileTransaction.js
// 這個就是 React 調度事務的 wrappers
var TRANSACTION_WRAPPERS = [
  // 確保在可能的狀況下,執行事務不會干擾選擇範圍(當前選定的文本輸入)。
  // 通俗點講就是,在 React 調度事務開始之時將選擇的文本信息獲取,再結束時還原選擇信息。
  // 關於 Selection 的具體實現這裏不作解讀。
  SELECTION_RESTORATION,
  // 抑制因爲高級別的DOM操做(如臨時從DOM中刪除文本輸入)而可能無心中調度的事件(模糊/焦點)。
  // 通俗點講就是,在 React 調度事務結束以前,抑制事件的傳播。
  // 於 core/ReactEventTopLevelCallback.js 中 createTopLevelCallback 函數,關於 _topLevelListenersEnabled 的判斷來抑制。
  // 事件控制,稍後作簡單解讀,但內容不會說起事件具體實現相關。
  EVENT_SUPPRESSION,
  // 提供 `ReactOnDOMReady` 隊列,用於在執行事務期間收集 `onDOMReady` 回調。
  ON_DOM_READY_QUEUEING
];

function ReactReconcileTransaction() {
  // 初始化事務
  this.reinitializeTransaction();
  // 這裏涉及到 PooledClass
  // 看到 ReactOnDOMReady 時末端也有這樣一句話:
  // PooledClass.addPoolingTo(ReactOnDOMReady)
  // 調用 getPooled 方法其實就是 new
  // 具體在下面解讀 PooledClass
  this.reactOnDOMReady = ReactOnDOMReady.getPooled(null);
}

var Mixin = {
  // reinitializeTransaction 方法中使用,用於獲取 React 調度事務的 wrappers
  getTransactionWrappers: function() {
    // 判斷執行環境是否可使用 DOM
    if (ExecutionEnvironment.canUseDOM) {
      return TRANSACTION_WRAPPERS;
    } else {
      return [];
    }
  },
  getReactOnDOMReady: function() {
    return this.reactOnDOMReady;
  },
  // 這裏也牽扯到 PooledClass
  destructor: function() {
    // PooledClass 中 standardReleaser 方法
    ReactOnDOMReady.release(this.reactOnDOMReady);
    // 釋放內存,一樣的,在 ReactOnDOMReady 中也進行了操做: this._queue = null
    this.reactOnDOMReady = null;
  }
};

mixInto(ReactReconcileTransaction, Transaction.Mixin);
mixInto(ReactReconcileTransaction, Mixin);

// 這是一個加入 PooledClass 的方法
PooledClass.addPoolingTo(ReactReconcileTransaction);
複製代碼

在看完 ReactReconcileTransaction 其實似懂非懂,不要緊,接下來要依次解讀 PooledClassReactOnDOMReady安全

工具方法 PooledClass

如下關於 PooledClass 僅解讀一個參數的方法,其他多參數狀況都是同樣的。數據結構

// utils/PooledClass.js
var oneArgumentPooler = function(copyFieldsFrom) {
  // 至關於執行 CopyConstructor.getPooled()
  // CopyConstructor 就是 addPoolingTo 傳入的第一參數
  var Klass = this;
  // 一但執行過 release , CopyConstructor.instancePool.length 纔會大於 0
  if (Klass.instancePool.length) {
    // 這個 instance 是作什麼用的,相信看到 call ,你變會理解。
    // 用來綁定執行上下文。
    var instance = Klass.instancePool.pop();
    // 這裏再也不進行實例化,而是以 instance 爲執行上下文直接調用,並將其返回。
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    // 至關於直接 new CopyConstructor(copyFieldsFrom)
    return new Klass(copyFieldsFrom);
  }
};

var standardReleaser = function(instance) {
  var Klass = this;
  // 在 CopyConstructor.getPooled() 後,保存返回結果
  // 在 release 時做爲參數傳入,若存在 destructor 則執行。
  if (instance.destructor) {
    instance.destructor();
  }
  // 長度未超過默認大小,則將 instance 推入數組
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};

// 默認的大小
var DEFAULT_POOL_SIZE = 10;
// 默認的 getPooled 方法
var DEFAULT_POOLER = oneArgumentPooler;

// 直接在 CopyConstructor 上添加屬性的方法,並返回 CopyConstructor
var addPoolingTo = function(CopyConstructor, pooler) {
  var NewKlass = CopyConstructor;
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  NewKlass.release = standardReleaser;
  return NewKlass;
};

var PooledClass = {
  addPoolingTo: addPoolingTo,
  oneArgumentPooler: oneArgumentPooler,
  twoArgumentPooler: twoArgumentPooler,
  fiveArgumentPooler: fiveArgumentPooler
};
複製代碼

要說 PooledClass 是作什麼用的,具體我也仍是沒有 get 到,可是通過個人實踐,在調用 var c = CopyConstructor.getPooled() 後,若在 c 上添加屬性,如: c.v = '0.3' 。而且在調用 CopyConstructor.release(c) 這樣的狀況,在從新進行 CopyConstructor.getPooled() 時,這個 v 屬性及值任然存在,固然前提是,你本身定義的 destructor 方法裏不會銷燬 v 的值。

React DOM 準備完成後的執行隊列

那麼如今咱們來看下 ReactOnDOMReady

// core/ReactOnDOMReady.js
// 初始化隊列
function ReactOnDOMReady(initialCollection) {
  this._queue = initialCollection || null;
}

mixInto(ReactOnDOMReady, {
  // 入隊
  enqueue: function(component, callback) {
    this._queue = this._queue || [];
    // 組件及回調
    this._queue.push({component: component, callback: callback});
  },
  // 執行隊列
  notifyAll: function() {
    var queue = this._queue;
    if (queue) {
      // 清空隊列
      this._queue = null;
      for (var i = 0, l = queue.length; i < l; i++) {
        var component = queue[i].component;
        var callback = queue[i].callback;
        // 回調傳入組件,上下文綁定 component
        // 關於 getDOMNode 方法稍後解讀(得到當前 component 的 node)
        callback.call(component, component.getDOMNode());
      }
      queue.length = 0;
    }
  },
  // 清空隊列
  reset: function() {
    this._queue = null;
  },
  // PooledClass.release 時候使用
  destructor: function() {
    this.reset();
  }
});

// 添加 PooledClass 方法
PooledClass.addPoolingTo(ReactOnDOMReady);
複製代碼

和事務結合

這段代碼很簡單了, ReactOnDOMReady 就是一個執行隊列。回顧到上面代碼發現, React 調度事務在被初始化的時候,一樣 ReactOnDOMReady 也被初始化,在調度事務執行 wrappers 的過程時, ReactOnDOMReady 被相繼執行。這個具體的過程,你看到 ON_DOM_READY_QUEUEING 變會明白。

// core/ReactReconcileTransaction.js
var ON_DOM_READY_QUEUEING = {
  // 在 React 調度事務 initialize 時, ReactOnDOMReady 隊列被重置。
  initialize: function() {
    this.reactOnDOMReady.reset();
  },
  // 在 React 調度事務 close 時, ReactOnDOMReady 隊列被依次執行。
  close: function() {
    this.reactOnDOMReady.notifyAll();
  }
};
複製代碼

一樣的,在 React 調度事務執行 release 時, ReactOnDOMReady 也會執行 release

getDOMNode 方法

差點忘了提下, getDOMNode 方法是作什麼用的,請直接看到源碼:

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    getDOMNode: function() {
      // 嘗試得到 _rootNode
      var rootNode = this._rootNode;
      if (!rootNode) {
        // 嘗試得到 _rootNodeID
        rootNode = document.getElementById(this._rootNodeID);
        if (!rootNode) {
          // TODO: Log the frequency that we reach this path.
          // 這裏代碼就不作詳細解讀了,反正就是爲了得到對應的 Node ,一級級往下查找。
          rootNode = ReactMount.findReactRenderedDOMNodeSlow(this._rootNodeID);
        }
        // 對其進行賦值,用於下次查詢。
        this._rootNode = rootNode;
      }
      return rootNode;
    },
  }
};
複製代碼

React 事務中的事件控制

那麼如今,咱們來看到 SELECTION_RESTORATION

// core/ReactReconcileTransaction.js
var EVENT_SUPPRESSION = {
  initialize: function() {
    // 獲取 ReactEvent.isEnabled() 用於 close 時接收。
    // 其實就是 true
    var currentlyEnabled = ReactEvent.isEnabled();
    // 設置爲 false
    ReactEvent.setEnabled(false);
    return currentlyEnabled;
  },
  // close 時設置爲 true
  close: function(previouslyEnabled) {
    ReactEvent.setEnabled(previouslyEnabled);
  }
};
複製代碼

那麼在這個真個 wrappers 執行過程,這個有什麼用呢? 這須要看到 ReactEventTopLevelCallback 的一段分支邏輯:

// core/ReactEventTopLevelCallback.js
var ReactEventTopLevelCallback = {
  createTopLevelCallback: function(topLevelType) {
    return function(fixedNativeEvent) {
      // setEnabled 方法修改的就是 _topLevelListenersEnabled 的值。
      if (!_topLevelListenersEnabled) {
        return;
      }
      // 直接掉事後續邏輯
    };
  }
};
複製代碼

回顧渲染 HTML 元素事務調度過程

那麼到此,咱們來回顧一下,以前說到的事務機制的運用是如何進行的,從新看到這段代碼是否是會清晰了不少:

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    mountComponentIntoNode: function(rootID, container) {
      // 初始化 React 調度事務
      var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
      // 進入 React 調度事務 wrappers 環節
      transaction.perform(
        this._mountComponentIntoNode,
        this,
        rootID,
        container,
        transaction
      );
      // 銷燬這個 React 調度事務
      ReactComponent.ReactReconcileTransaction.release(transaction);
      // 整個掛載方法結束
    }
  }
}
複製代碼

這裏附一張此方法的圖:

/** * <pre> * TRANSACTION_WRAPPERS (ExecutionEnvironment.canUseDOM 爲 true 的狀況) * + + + * | | | * +-----------------|----------------|-----|-------------------+ * | v | | | * | +-----------------------+ | | | * | +--| SELECTION_RESTORATION |---|-----|---+ | * | | +-----------------------+ v | | | * | | +-------------------+ | | | * | | +-------| EVENT_SUPPRESSION |--|---|-----+ | * | | | +-------------------+ v | | | * | | | +-----------------------+ | | | * | | | +--| ON_DOM_READY_QUEUEING |-|-----|-----+ | * | | | | +-----------------------+ | | | | * | | | | | | | | * perform(_mount | v v v v v v | wrapper * Component | +---+ +---+ +---+ +----------------+ +---+ +---+ +---+ | invariants * IntoNode) | | | | | | | | | | | | | | | | maintained * +------------------>|-|---|-|---|-|---|-->| _mount |---|---|-|---|-|---|-|--------> * | | | | | | | | Component | | | | | | | | * | | | | | | | | IntoNode | | | | | | | | * | | | | | | | | | | | | | | | | * | +---+ +---+ +---+ +----------------+ +---+ +---+ +---+ | * | initialize close | * +------------------------------------------------------------+ * </pre> */
複製代碼

那麼至此,實現事務機制及 React 調度事務。


關於 Reconciler

Stack Reconciler

咱們知道瀏覽器渲染引擎是單線程的,在 React 15.x 版本及以前版本,計算組件樹變動時將會阻塞整個線程,整個渲染過程是連續不中斷完成的,而這時的其餘任務都會被阻塞,如動畫等,這可能會使用戶感受到明顯卡頓,好比當你在訪問某一網站時,輸入某個搜索關鍵字,更優先的應該是交互反饋或動畫效果,若是交互反饋延遲 200ms ,用戶則會感受較明顯的卡頓,而數據響應晚200毫秒並沒太大問題。這個版本的調和器能夠稱爲棧調和器( Stack Reconciler ),其調和算法大體過程見 React Diff 算法React Stack Reconciler 實現

Stack Reconcilier 的主要缺陷就是不能暫停渲染任務,也不能切分任務,沒法有效平衡組件更新渲染與動畫相關任務間的執行順序,即不能劃分任務優先級,有可能致使重要任務卡頓,動畫掉幀等問題。

Fiber Reconciler

React 16 版本提出了一個更先進的調和器,它容許渲染進程分段完成,而沒必要須一次性完成,中間能夠返回至主進程控制執行其餘任務。而這是經過計算部分組件樹的變動,並暫停渲染更新,詢問主進程是否有更高需求的繪製或者更新任務須要執行,這些高需求的任務完成後纔開始渲染。這一切的實現是在代碼層引入了一個新的數據結構 - Fiber 對象,每個組件實例對應有一個 fiber 實例,此 fiber 實例負責管理組件實例的更新,渲染任務及與其餘 fiber 實例的聯繫。

這個新推出的調和器就叫作纖維調和器( Fiber Reconciler ),它提供的新功能主要有:

  1. 可切分,可中斷任務;
  2. 可重用各分階段任務,且能夠設置優先級;
  3. 能夠在父子組件任務間前進後退切換任務;
  4. render 方法能夠返回多元素(便可以返回數組);
  5. 支持異常邊界處理異常;

參閱《React Fiber初探》

相關文章
相關標籤/搜索