窺探React-源碼分析(二)

上一篇文章講到了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.jsvar 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.jsfunction 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方法:

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


推薦閱讀:

前後分離兼攻守,幹活帶風嗖嗖嗖
react 行,我等你
react v16使用flow做類型檢查,Fiber使用typescript做類型聲明以及測試?
截止到2017年7月,手淘內部還在用vue嗎,有替換成react嗎?
什麼時候react.js合適使用redux與不使用?

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