窺探React - 源碼分析

所謂知其然還要知其因此然. 本文將分析 React 15-stable的部分源碼, 包括組件初始渲染的過程和組件更新的過程.在這以前, 假設讀者已經:javascript

  • 對React有必定了解
  • 知道React element、component、class區別
  • 瞭解生命週期、事務、批量更新、virtual DOM大體概念等

如何分析 React 源碼

代碼架構預覽html

首先, 咱們找到React在Github上的地址, 把15-stable版本的源碼copy下來, 觀察它的總體架構, 這裏首先閱讀關於源碼介紹的官方文檔, 再接着看.前端

咱們 要分析的源碼在 src 目錄下:java

// src 部分目錄

├── ReactVersion.js    # React版本號
├── addons             # 插件
├── isomorphic           # 同構代碼,做爲react-core, 提供頂級API
├── node_modules
├── package.json
├── renderers          # 渲染器, 包括DOM,Native,art,test等
├── shared             # 子目錄之間須要共享的代碼,提到父級目錄shared
├── test               # 測試代碼

分析方法node

一、首先看一些網上分析的文章, 對重點部分的源碼有個印象, 知道一些關鍵詞意思, 避免在無關的代碼上迷惑、耗費時間;react

二、準備一個demo, 無任何功能代碼, 只安裝react,react-dom, Babel轉義包, 避免分析無關代碼;json

三、打debugger; 利用Chrome devtool一步一步走, 打斷點, 看調用棧,看函數返回值, 看做用域變量值;瀏覽器

四、利用編輯器查找代碼、閱讀代碼等安全

正文

咱們知道, 對於通常的React 應用, 瀏覽器會首先執行代碼 ReactDOM.render來渲染頂層組件, 在這個過程當中遞歸渲染嵌套的子組件, 最終全部組件被插入到DOM中. 咱們來看看架構

調用ReactDOM.render 發生了什麼

大體過程(只展現主要的函數調用):

React render

若是看不清這有矢量圖

讓咱們來分析一下具體過程:


一、建立元素

首先, 對於你寫的jsx, Babel會把這種語法糖轉義成這樣:

// jsx
ReactDOM.render(
    <C />,
    document.getElementById('app')
)

// 轉義後
ReactDOM.render(
  React.createElement(C, null), 
  document.getElementById('app')
);

沒錯, 就是調用React.createElement來建立元素. 元素是什麼? 元素只是一個對象描述了DOM樹, 它像這樣:

{
  $$typeof: Symbol(react.element)
  key: null
  props: {}        // props有child屬性, 描述子組件, 一樣是元素
  ref: null
  type: class C    // type能夠是類(自定義組件)、函數(wrapper)、string(DOM節點)
  _owner: null
  _store: {validated: false}
  _self: null
  _source: null
  __proto__: Object
}

React.createElement源碼在ReactElement.js中, 邏輯比較簡單, 不作分析.

二、實例化組件

建立出來的元素被看成參數和指定的 DOM container 一塊兒傳進ReactDOM.render. 接下來會調用一些內部方法, 接着調用了 instantiateReactComponent, 這個函數根據element的類型實例化對應的component. 當element的類型爲:

  • string時, 說明是文本, 實例化ReactDOMTextComponent;
  • ReactElement時, 說明是react元素, 進一步判斷element.type的類型, 當爲

    • string時, 爲DOM原生節點, 實例化ReactDOMComponent;
    • 函數或類時, 爲react 組件, 實例化ReactCompositeComponent

instantiateReactComponent函數在instantiateReactComponent.js :

/**
 * Given a ReactNode, create an instance that will actually be mounted.
 */
function instantiateReactComponent(node(這裏node指element), shouldHaveDebugID) {
  ...
  
  // 若是element爲空
  if (node === null || node === false) {
    // 建立空component
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {  // 若是是對象
      ...     // 這裏是類型檢查
   
    // 若是element.type是字符串
    if (typeof element.type === 'string') {
      //實例化 宿主組件, 也就是DOM節點
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // 保留給之後版本使用,此處暫時不會涉及到
    } else { // 不然就實例化ReactCompositeComponent
      instance = new ReactCompositeComponentWrapper(element);
    }
  // 若是element是string或number
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 實例化ReactDOMTextComponent
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    invariant(false, 'Encountered invalid React node of type %s', typeof node);
  }
   ...
  return instance;
}
三、批量更新

在調用instantiateReactComponent拿到組件實例後, React 接着調用了batchingStrategy.batchedUpdates並將組件實例看成參數執行批量更新.

批量更新是一種優化策略, 避免重複渲染, 在不少框架都存在這種機制. 其實現要點是要弄清楚什麼時候存儲更新, 什麼時候批量更新.

在React中, 批量更新受batchingStrategy控制,而這個策略除了server端都是ReactDefaultBatchingStrategy:

不信你看, 在ReactUpdates.js中 :

var ReactUpdatesInjection = {
  ...
  // 注入批量策略的函數聲明
  injectBatchingStrategy: function(_batchingStrategy) {
    ... 
  
    batchingStrategy = _batchingStrategy;
  },
};

在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :

ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy); // 注入

那麼React是如何實現批量更新的? 在ReactDefaultBatchingStrategy.js咱們看到, 它的實現依靠了事務.

3.1 咱們先介紹一下事務.

在 Transaction.js中, React 介紹了事務:

* <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

React 把要調用的函數封裝一層wrapper, 這個wrapper通常是一個對象, 裏面有initialize方法, 在調用函數前調用;有close方法, 在函數執行後調用. 這樣封裝的目的是爲了, 在要調用的函數執行先後某些不變性約束條件(invariant)仍然成立.

這裏的不變性約束條件(invariant), 我把它理解爲 「真命題」, 所以前面那句話意思就是, 函數調用先後某些規則仍然成立. 好比, 在調和(reconciliation)先後保留UI組件一些狀態.

React 中, 事務就像一個黑盒, 函數在這個黑盒裏被執行, 執行先後某些規則仍然成立, 即便函數報錯. 事務提供了函數執行的一個安全環境.

繼續看Transaction.js對事務的抽象實現:

// 事務的抽象實現, 做爲基類
var TransactionImpl = {
  // 初始化/重置實例屬性, 給實例添加/重置幾個屬性, 實例化事務時會調用
  reinitializeTransaction: function () {
    this.transactionWrappers = this.getTransactionWrappers();
    if (this.wrapperInitData) {
      this.wrapperInitData.length = 0;
    } else {
      this.wrapperInitData = [];
    }
    this._isInTransaction = false;
  },

  _isInTransaction: false,

  // 這個函數會交給具體的事務實例化時定義, 初始設爲null
  getTransactionWrappers: null,
  // 判斷是否已經在這個事務中, 保證當前的Transaction正在perform的同時不會再次被perform
  isInTransaction: function () {
    return !!this._isInTransaction;
  },
  
  // 頂級API, 事務的主要實現, 用來在安全的窗口下執行函數
  perform: function (method, scope, a, b, c, d, e, f) {
    var ret;
    var errorThrown;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      this.initializeAll(0);  // 調用全部wrapper的initialize方法
      ret = method.call(scope, a, b, c, d, e, f); // 調用要執行的函數
      errorThrown = false;
    } finally {
      // 調用全部wrapper的close方法, 利用errorThrown標誌位保證只捕獲函數執行時的錯誤, 對initialize      // 和close拋出的錯誤不作處理
      try {
        if (errorThrown) {
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },
    
  // 調用全部wrapper的initialize方法的函數定義
  initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers; // 獲得wrapper
    // 遍歷依次調用
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        ...
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this):null;
      } finally {
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

  // 調用全部wrapper的close方法的函數定義
  closeAll: function (startIndex) {
    ...
    var transactionWrappers = this.transactionWrappers; // 拿到wrapper
    // 遍歷依次調用
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        ...
        if (initData !== OBSERVED_ERROR && wrapper.close) {
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          ...
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }
};

好的, 相信你已經對事務如何實現有了大體瞭解, 但這只是React事務的抽象實現, 還須要實例化事務並對其增強的配合, 才能發揮事務的真正做用.

3.2 批量更新依靠了事務

剛講到, 在React中, 批量更新受batchingStrategy控制,而這個策略除了server端都是ReactDefaultBatchingStrategy, 而在ReactDefaultBatchingStrategy.js中, 批量更新的實現依靠了事務:

ReactDefaultBatchingStrategy.js :

...
var Transaction = require('Transaction');// 引入事務
...

var RESET_BATCHED_UPDATES = {   // 重置的 wrapper
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {  // 批處理的 wrapper
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

// 組合成 ReactDefaultBatchingStrategyTransaction 事務的wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; 

// 調用 reinitializeTransaction 初始化
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

// 參數中依賴了事務
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction(); // 實例化這類事務

// 批處理策略
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

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

    // 避免重複分配事務
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);  // 將callback放進事務裏執行
    }
  },
};

那麼, 爲何批量更新的實現依靠了事務呢? 還記得實現批量更新的兩個要點嗎?

  • 什麼時候存儲更新
  • 什麼時候批處理

對於這兩個問題, React 在執行事務時調用wrappers的initialize方法, 創建更新隊列, 而後執行函數 :

  • 什麼時候存儲更新—— 在執行函數時遇到更新請求就存到這個隊列中
  • 什麼時候批處理—— 函數執行後調用wrappers的close方法, 在close方法中調用批量處理函數

口說無憑, 得有證據. 咱們拿ReactDOM.render會調用的事務ReactReconcileTransaction來看看是否是這樣:

ReactReconcileTransaction.js 裏有個wrapper, 它是這樣定義的(英文是官方註釋) :

var ON_DOM_READY_QUEUEING = {
  /**
   * Initializes the internal `onDOMReady` queue.
   */
  initialize: function() {
    this.reactMountReady.reset();
  },

  /**
   * After DOM is flushed, invoke all registered `onDOMReady` callbacks.
   */
  close: function() {
    this.reactMountReady.notifyAll();
  },
};

咱們再看ReactReconcileTransaction事務會執行的函數mountComponent, 它在

ReactCompositeComponent.js :

/*
   * Initializes the component, renders markup, and registers event listeners.
*/
  mountComponent: function(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
  ) {
    ...
    
    if (inst.componentDidMount) {
          if (__DEV__) {
            transaction.getReactMountReady().enqueue(() => { // 將要調用的callback存起來
              measureLifeCyclePerf(
                () => inst.componentDidMount(),
                this._debugID,
                'componentDidMount',
              );
            });
          } else {
            transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
          }
      }
      
     ...
    }

而上述wrapper定義的close方法調用的this.reactMountReady.notifyAll()在這

CallbackQueue.js :

/**
   * Invokes all enqueued callbacks and clears the queue. This is invoked after
   * the DOM representation of a component has been created or updated.
   */
  notifyAll() {
      ...
      // 遍歷調用存儲的callback
      for (var i = 0; i < callbacks.length; i++) {
        callbacks[i].call(contexts[i], arg);
      }
      callbacks.length = 0;
      contexts.length = 0;
    }
  }

你居然讀到這了

好累(笑哭), 先寫到這吧. 我原本還想一篇文章就把組件初始渲染的過程和組件更新的過程講完, 如今看來要分開講了… React 細節太多了, 蘊含的信息量也很大…說博大精深一點不誇張...向React的做者們以及社區的人們致敬!

我以爲讀源碼是一件很費力可是很是值得的事情. 剛開始讀的時候一點頭緒也沒有, 不知道它是什麼樣的過程, 不知道爲何要這麼寫, 有時候還會由於斷點沒打好繞了不少彎路…也是硬着頭皮一遍一遍看, 結合網上的文章, 就這樣雲裏霧裏的慢慢摸索, 不斷更正本身的認知.後來看多了, 就常常會有大徹大悟的感受, 零碎的認知開始連通起來, 逐漸摸清了前因後果.

如今以爲確實很值得, 本身學到了很多. 看源碼的過程就感受是跟做者們交流討論同樣, 思想在碰撞! 強烈推薦前端的同窗們閱讀React源碼, 大神們智慧的結晶!

未完待續...

相關文章
相關標籤/搜索