窺探React - 源碼分析

所謂知其然還要知其所以然. 本系列文章將分析 React 15-stable的部分源碼, 包括組件初始渲染的過程、組件更新的過程等. 這篇文章先介紹組件初始渲染的過程的幾個重要概念, 包括大致過程、創建元素、實例化組件、事務、批量更新策略等. 在這之前, 假設讀者已經:

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

如何分析 React 源碼

代碼架構預覽

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

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

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

分析方法

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

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

3、打debugger; 利用Chrome devtool一步一步走, 打斷點, 看調用棧,看函數返回值, 看作用域變數值;

4、利用編輯器查找代碼、閱讀代碼等

正文

我們知道, 對於一般的React 應用, 瀏覽器會首先執行代碼 ReactDOM.render來渲染頂層組件, 在這個過程中遞歸渲染嵌套的子組件, 最終所有組件被插入到DOM中. 我們來看看

調用ReactDOM.render 發生了什麼

大致過程(只展示主要的函數調用):

如果看不清這有矢量圖

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

1、創建元素

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

// jsxReactDOM.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中, 邏輯比較簡單, 不做分析.

2、創建對應類型的React組件

創建出來的元素被當作參數和指定的 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;}

3、批量更新

在調用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事務的抽象實現(基類), 還需要實例化事務並對其加強的配合, 才能發揮事務的真正作用. 另外, 在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; // 事務結束即一次batch結束 },};var FLUSH_BATCHED_UPDATES = { // 批處理的 wrapper initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),};// 組合成 ReactDefaultBatchingStrategyTransaction 事務的wrappervar 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, // 是否處在一次BatchingUpdates標誌位 // 批量更新策略調用的就是這個方法 batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; // 一旦調用批處理, 重置isBatchingUpdates標誌位, 表示正處在一次BatchingUpdates中 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源碼, 大神們智慧的結晶!

未完待續...


推薦閱讀:

直播系統源碼:萬萬沒想到!直播行業二三事
express源碼中的對象繼承
窺探React-源碼分析(二)
基於CentOS6.4環境編譯Spark-2.1.0源碼
如何看待一加違反開源協議不公開 DASH 快充代碼?

TAG:前端框架 | React | 源碼 |