React 源碼解析

作者:百度外賣 陳昊陽

轉載請標明出處

前言

  • 適合有一定 React 項目經驗閱讀,默認對 React 的常用 api 較為熟悉
  • 研究 React 源碼是結合網上的一些分析文章+自己看代碼理解
  • 最開始看是因為項目中遇到性能問題,網上沒有相關資料,所以想找到具體影響的點
  • 以下的代碼解析以 15.4.1 版本為基礎,且去除了開發環境的 warning,為了區分,保留的注釋都為英文,新增的注釋為中文,盡量保持原注釋
  • 文中有部分自己的演繹、理解、猜測,如有誤煩請指出

基礎概念

  • ReactElement
    • 數據類,只包含 props refs key 等
    • 由 React.creatElement(ReactElement.js) 創建,React.createClass 中 render 中返回的實際也是個 ReactElement
  • ReactComponent
    • 控制類,包含組件狀態,操作方法等
    • 包括字元組件、原生 DOM 組件、自定義組件(和空組件)
    • 在掛載組件(mountComponent)的時候,會調用到 instantiateReactComponent 方法,利用工廠模式,通過不同的輸入返回不同的 component
    • 代碼(instantiateReactComponent.js):

function instantiateReactComponent(node, shouldHaveDebugID) {var instance;if (node === null || node === false) {instance = ReactEmptyComponent.create(instantiateReactComponent);} else if (typeof node === object) {var element = node;// Special case string valuesif (typeof element.type === string) { instance = ReactHostComponent.createInternalComponent(element);} else if (isInternalComponentType(element.type)) { // This is temporarily available for custom components that are not string // representation, we can drop this code path.} else { instance = new ReactCompositeComponentWrapper(element);}} else if (typeof node === string || typeof node === number) {instance = ReactHostComponent.createInstanceForText(node);} else {}// These two fields are used by the DOM and ART diffing algorithms// respectively. Instead of using expandos on components, we should be// storing the state needed by the diffing algorithms elsewhere.instance._mountIndex = 0;instance._mountImage = null;return instance;}

    • ReactDOMTextComponent 只關心文本,ReactDOMComponent 會稍微簡單一些,ReactCompositeComponent 需要關心的最多,包括得到原生 DOM 的渲染內容
  • ReactClass
    • 這個比較特殊,對比 ES5 寫法: var MyComponent = React.createClass({}),ES6寫法:class MyComponent extends React.Component,為什麼用createClass卻得到了Component呢?通過源碼來看,這兩個 api 的實現幾乎是一樣的,也可以看到,ES6 的寫法簡潔的多,不用那些getInitialState等特定 api,React 在之後的版本也會拋棄createClass這個 api。並且,在此 api 中,React 進行了autobind。
    • ReactClass.js:

var ReactClass = { createClass: function (spec) { // ensure that Constructor.name !== Constructor var Constructor = identity(function (props, context, updater) { // Wire up auto-binding if (this.__reactAutoBindPairs.length) { bindAutoBindMethods(this); } this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; this.state = null; // ReactClasses doesnt have constructors. Instead, they use the // getInitialState and componentWillMount methods for initialization. var initialState = this.getInitialState ? this.getInitialState() : null; this.state = initialState; }); Constructor.prototype = new ReactClassComponent(); Constructor.prototype.constructor = Constructor; Constructor.prototype.__reactAutoBindPairs = []; injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor)); mixSpecIntoComponent(Constructor, spec); // Initialize the defaultProps property after all mixins have been merged. if (Constructor.getDefaultProps) { Constructor.defaultProps = Constructor.getDefaultProps(); } // Reduce time spent doing lookups by setting these on the prototype. for (var methodName in ReactClassInterface) { if (!Constructor.prototype[methodName]) { Constructor.prototype[methodName] = null; } } return Constructor; }}var ReactClassComponent = function () {};_assign(ReactClassComponent.prototype, ReactComponent.prototype, ReactClassMixin);

ReactComponent.js:

function ReactComponent(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue;}ReactComponent.prototype.isReactComponent = {};ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, setState); }};ReactComponent.prototype.forceUpdate = function (callback) { this.updater.enqueueForceUpdate(this); if (callback) { this.updater.enqueueCallback(this, callback, forceUpdate); }};

對象池

  • 開闢空間是需要一定代價的
  • 如果引用釋放而進入 gc,gc 會比較消耗性能和時間,如果內存抖動(大量的對象被創建又在短時間內馬上被釋放)而頻繁 gc 則會影響用戶體驗
  • 既然創建和銷毀對象是很耗時的,所以要儘可能減少創建和銷毀對象的次數
  • 使用時候申請(getPooled)和釋放(release)成對出現,使用一個對象後一定要釋放還給池子(釋放時候要對內部變數置空方便下次使用)
  • 代碼(PooledClass.js):

// 只展示部分var oneArgumentPooler = function (copyFieldsFrom) { var Klass = this; if (Klass.instancePool.length) { var instance = Klass.instancePool.pop(); Klass.call(instance, copyFieldsFrom); return instance; } else { return new Klass(copyFieldsFrom); }};var standardReleaser = function (instance) { var Klass = this; if (Klass.instancePool.length < Klass.poolSize) { Klass.instancePool.push(instance); }};var DEFAULT_POOL_SIZE = 10;var DEFAULT_POOLER = oneArgumentPooler;var addPoolingTo = function (CopyConstructor, pooler) { // Casting as any so that flow ignores the actual implementation and trusts // it to match the type we declared 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, threeArgumentPooler: threeArgumentPooler, fourArgumentPooler: fourArgumentPooler, fiveArgumentPooler: fiveArgumentPooler};module.exports = PooledClass;

使用例子(ReactUpdate.js):

var transaction = ReactUpdatesFlushTransaction.getPooled();destructor: function () { this.dirtyComponentsLength = null; CallbackQueue.release(this.callbackQueue); this.callbackQueue = null; ReactUpdates.ReactReconcileTransaction.release(this.reconcileTransaction); this.reconcileTransaction = null; }ReactUpdatesFlushTransaction.release(transaction);

  • 可以看到,如果短時間內生成了大量的對象佔滿了池子,後續的對象是不能復用只能新建的
  • 對比連接池、線程池:完成任務後並不銷毀,而是可以復用去執行其他任務

事務機制

  • React 通過事務機制來完成一些特定操作,比如 merge state,update component
  • 示意圖(Transaction.js):

代碼(Transaction.js):

var TransactionImpl = { perform: function (method, scope, a, b, c, d, e, f) { var errorThrown; var ret; try { this._isInTransaction = true; // Catching errors makes debugging more difficult, so we start with // errorThrown set to true before setting it to false after calling // close -- if its still set to true in the finally block, it means // one of these calls threw. errorThrown = true; this.initializeAll(0); ret = method.call(scope, a, b, c, d, e, f); errorThrown = false; } finally { try { if (errorThrown) { // If `method` throws, prefer to show that stack trace over any thrown // by invoking `closeAll`. try { this.closeAll(0); } catch (err) {} } else { // Since `method` didnt throw, we dont want to silence the exception // here. this.closeAll(0); } } finally { this._isInTransaction = false; } } return ret; }, // 執行所有 wrapper 中的 initialize 方法 initializeAll: function (startIndex) { }, // 執行所有 wrapper 中的 close 方法 closeAll: function (startIndex) { }};module.exports = TransactionImpl;

  • 可以看到和後端的事務是有差異的(有點類似AOP),雖然都叫transaction,並沒有commit,而是自動執行,初始方法沒有提供rollback,有二次封裝提供的(ReactReconcileTransaction.js)
  • 下文會提到事務機制的具體使用場景

事件分發

  • 框圖(ReactBrowserEventEmitter.js)

  • 組件上聲明的事件最終綁定到了 document 上,而不是 React 組件對應的 DOM 節點,這樣簡化了 DOM 原生事件,減少了內存開銷
  • 以隊列的方式,從觸發事件的組件向父組件回溯,調用相應 callback,也就是 React 自身實現了一套事件冒泡機制,雖然 React 對合成事件封裝了stopPropagation,但是並不能阻止自己手動綁定的原生事件的冒泡,所以項目中要避免手動綁定原生事件
  • 使用對象池來管理合成事件對象的創建和銷毀,好處在上文中有描述
  • ReactEventListener:負責事件註冊和事件分發
  • ReactEventEmitter:負責事件執行
  • EventPluginHub:負責事件的存儲,具體存儲在listenerBank
  • Plugin: 根據不同的事件類型,構造不同的合成事件,可以連接原生事件和組件
  • 當事件觸發時,會調用ReactEventListener.dispatchEvent,進行分發:找到具體的 ReactComponent,然後向上遍歷父組件,實現冒泡
  • 代碼較多,就不具體分析了,這種統一收集然後分發的思路,可以用在具體項目中

生命周期

  • 整體流程:

  • 主要講述mount和update,裡面也有很多相類似的操作
  • componentWillMount,render,componentDidMount 都是在 mountComponent 中被調用
  • 分析 ReactCompositeComponent.js 中的mountComponent,發現輸出是@return {?string} Rendered markup to be inserted into the DOM.

mountComponent: function (transaction, hostParent, hostContainerInfo, context) { var _this = this; this._context = context; this._mountOrder = nextMountID++; this._hostParent = hostParent; this._hostContainerInfo = hostContainerInfo; var publicProps = this._currentElement.props; var publicContext = this._processContext(context); var Component = this._currentElement.type; var updateQueue = transaction.getUpdateQueue(); // Initialize the public class var doConstruct = shouldConstruct(Component); // 最終會調用 new Component() var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue); var renderedElement; // Support functional components if (!doConstruct && (inst == null || inst.render == null)) { renderedElement = inst; inst = new StatelessComponent(Component); this._compositeType = CompositeTypes.StatelessFunctional; } else { // 大家經常在用戶端用到的 PureComponent,會對 state 進行淺比較然後決定是否執行 render if (isPureComponent(Component)) { this._compositeType = CompositeTypes.PureClass; } else { this._compositeType = CompositeTypes.ImpureClass; } } // These should be set up in the constructor, but as a convenience for // simpler class abstractions, we set them up after the fact. inst.props = publicProps; inst.context = publicContext; inst.refs = emptyObject; inst.updater = updateQueue; this._instance = inst; // Store a reference from the instance back to the internal representation // 以 element 為 key,存在了 Map 中,之後會用到 ReactInstanceMap.set(inst, this); var initialState = inst.state; if (initialState === undefined) { inst.state = initialState = null; } this._pendingStateQueue = null; this._pendingReplaceState = false; this._pendingForceUpdate = false; var markup; if (inst.unstable_handleError) { markup = this.performInitialMountWithErrorHandling(renderedElement, hostParent, hostContainerInfo, transaction, context); } else { markup = this.performInitialMount(renderedElement, hostParent, hostContainerInfo, transaction, context); } if (inst.componentDidMount) { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } return markup; }function shouldConstruct(Component) { return !!(Component.prototype && Component.prototype.isReactComponent);}

  • 可以看到,mountComponet 先做實例對象的初始化(props, state 等),然後調用performInitialMount掛載(performInitialMountWithErrorHandling最終也會調用performInitialMount,只是多了錯誤處理),然後調用componentDidMount
  • transaction.getReactMountReady()會得到CallbackQueue,所以只是加入到隊列中,後續執行
  • 我們來看performInitialMount(依然在 ReactCompositeComponent.js 中)

performInitialMount: function (renderedElement, hostParent, hostContainerInfo, transaction, context) { var inst = this._instance; var debugID = 0; if (inst.componentWillMount) { inst.componentWillMount(); // When mounting, calls to `setState` by `componentWillMount` will set // `this._pendingStateQueue` without triggering a re-render. if (this._pendingStateQueue) { inst.state = this._processPendingState(inst.props, inst.context); } } // If not a stateless component, we now render // 返回 ReactElement,這也就是上文說的 render 返回 ReactElement if (renderedElement === undefined) { renderedElement = this._renderValidatedComponent(); } var nodeType = ReactNodeTypes.getType(renderedElement); this._renderedNodeType = nodeType; var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY); this._renderedComponent = child; var markup = ReactReconciler.mountComponent(child, transaction, hostParent, hostContainerInfo, this._processChildContext(context), debugID); return markup; }

  • performInitialMount 中先調用componentWillMount,這個過程中 merge state,然後調用_renderValidatedComponent(最終會調用inst.render() )返回 ReactElement,然後調用_instantiateReactComponent 由 ReactElement 創建 ReactComponent,最後進行遞歸渲染。
  • 掛載之後,可以通過setState來更新(機制較為複雜,後文會單獨分析),此過程通過調用updateComponent來完成更新。我們來看updateComponent(依然在 ReactCompositeComponent.js 中)

updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) { var inst = this._instance; var willReceive = false; var nextContext; // context 相關,React 建議少用 context // Determine if the context has changed or not if (this._context === nextUnmaskedContext) { nextContext = inst.context; } else { nextContext = this._processContext(nextUnmaskedContext); willReceive = true; } var prevProps = prevParentElement.props; var nextProps = nextParentElement.props; // Not a simple state update but a props update if (prevParentElement !== nextParentElement) { willReceive = true; } // An update here will schedule an update but immediately set // _pendingStateQueue which will ensure that any state updates gets // immediately reconciled instead of waiting for the next batch. if (willReceive && inst.componentWillReceiveProps) { inst.componentWillReceiveProps(nextProps, nextContext); } var nextState = this._processPendingState(nextProps, nextContext); var shouldUpdate = true; if (!this._pendingForceUpdate) { if (inst.shouldComponentUpdate) { shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext); } else { if (this._compositeType === CompositeTypes.PureClass) { // 這裡,就是上文提到的,PureComponent 里的淺比較 shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } } } this._updateBatchNumber = null; if (shouldUpdate) { this._pendingForceUpdate = false; // Will set `this.props`, `this.state` and `this.context`. this._performComponentUpdate(nextParentElement, nextProps, nextState, nextContext, transaction, nextUnmaskedContext); } else { // If its determined that a component should not update, we still want // to set props and state but we shortcut the rest of the update. this._currentElement = nextParentElement; this._context = nextUnmaskedContext; inst.props = nextProps; inst.state = nextState; inst.context = nextContext; } }

  • updateComponent中,先調用componentWillReceiveProps,然後 merge state,然後調用shouldComponentUpdate判斷是否需要更新,可以看到,如果組件內部沒有自定義,且用的是 PureComponent,會對 state 進行淺比較,設置shouldUpdate,最終調用_performComponentUpdate來進行更新。而在_performComponentUpdate中,會先調用componentWillUpdate,然後調用updateRenderedComponent進行更新,最後調用componentDidUpdate(過程較簡單,就不列代碼了)。下面看一下updateRenderedComponent的更新機制(依然在 ReactCompositeComponent.js 中)

_updateRenderedComponent: function (transaction, context) { var prevComponentInstance = this._renderedComponent; var prevRenderedElement = prevComponentInstance._currentElement; var nextRenderedElement = this._renderValidatedComponent(); var debugID = 0; if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) { ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context)); } else { var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance); ReactReconciler.unmountComponent(prevComponentInstance, false); var nodeType = ReactNodeTypes.getType(nextRenderedElement); this._renderedNodeType = nodeType; var child = this._instantiateReactComponent(nextRenderedElement, nodeType !== ReactNodeTypes.EMPTY); this._renderedComponent = child; var nextMarkup = ReactReconciler.mountComponent(child, transaction, this._hostParent, this._hostContainerInfo, this._processChildContext(context), debugID); this._replaceNodeWithMarkup(oldHostNode, nextMarkup, prevComponentInstance); } },

可以看到,如果需要更新,則調用ReactReconciler.receiveComponent,會遞歸更新子組件,否則直接卸載然後掛載。所以,重點是在shouldUpdateReactComponent的判斷,React 為了簡化 diff,所以有一個假設:在組件層級、type、key 不變的時候,才進行比較更新,否則先 unmount 然後重新 mount。來看shouldUpdateReactComponent(shouldUpdateReactComponent.js) :

function shouldUpdateReactComponent(prevElement, nextElement) { var prevEmpty = prevElement === null || prevElement === false; var nextEmpty = nextElement === null || nextElement === false; if (prevEmpty || nextEmpty) { return prevEmpty === nextEmpty; } var prevType = typeof prevElement; var nextType = typeof nextElement; // 如果前後兩次都為文本元素,則更新 if (prevType === string || prevType === number) { return nextType === string || nextType === number; } else { // 如果為 ReactDomComponent 或 ReactCompositeComponent,則需要層級 type 和 key 相同,才進行 update(層級在遞歸中保證相同) return nextType === object && prevElement.type === nextElement.type && prevElement.key === nextElement.key; }}

接下來是重頭戲:setState,上文中已經提到了此 api 為:

ReactComponent.prototype.setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, setState); }};

可以看到這裡只是簡單的調用enqueueSetState放入隊列中,而我們知道,不可能這麼簡單的。來看enqueueSetState(ReactUpdateQueue.js中),this.updater會在 mount 時候賦值為updateQueue

enqueueSetState: function (publicInstance, partialState) { var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, setState); if (!internalInstance) { return; } // 獲取隊列,如果為空則創建 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); // 將待 merge 的 state 放入隊列 queue.push(partialState); // 將待更新的組件放入隊列 enqueueUpdate(internalInstance); }, function getInternalInstanceReadyForUpdate(publicInstance, callerName) { // 上文提到的以 element 為 key 存入 map,這裡可以取到 component var internalInstance = ReactInstanceMap.get(publicInstance); if (!internalInstance) { return null; } return internalInstance; }

再來看enqueueUpdate(ReactUpdates.js):

function enqueueUpdate(component) { if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; }}

  • 可以看到,如果不處於isBatchingUpdates時,則調用batchingStrategy.batchedUpdates,如果處於的話,則將 component 放入 dirtyComponents 中等待以後處理。這樣保證了避免重複 render,因為mountComponent和updateComponent 執行的開始,會將isBatchingUpdates 設置為true,之後以事務的方式處理,包括最後時候將isBatchingUpdates置為false。
  • 大家一定對 batchingStrategy 和 dirtyComponents 的定義,batchingStrategy由ReactUpdates.injection 注入,而dirtyComponents 是定義在 ReactUpdates.js 中,也就是說二者都為全局的
  • 綜上,在特定生命周期中,如 getInitialState,componentWillMount,render,componentWillUpdate 中調用setState,並不會引起updateComponent(componentDidMount、componentDidUpdate 中會)。來看batchedUpdates(ReactDefaultBatchingStrategy.js):

batchedUpdates: function (callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { // 注意這裡,上一個代碼塊中可以看到,當 isBatchingUpdates 為 false 時,callback 為 enqueueUpdate 自身 // 所以即以事務的方式處理 return transaction.perform(callback, null, a, b, c, d, e); } } var transaction = new ReactDefaultBatchingStrategyTransaction();

  • 可以看到,當以事務的方式調用進入enqueueUpdate時,isBatchingUpdates已經為true,所以執行dirtyComponents.push(component);。
  • 注意到callbakc其實就是自身enqueueUpdate,當isBatchingUpdates為false時,也用transaction.perform調用enqueueUpdate,使得結果一樣
  • 詳細介紹事務 transaction 的應用,上文中提到過,事務可以利用wrapper封裝,開始和結束時會調用所有 wrapper 的相應方法,來看這兩個wrapper: RESET_BATCHED_UPDATES FLUSH_BATCHED_UPDATES(ReactDefaultBatchingStrategy.js):

var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function () { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }};var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)};// flushBatchedUpdates 在 ReactUpdates.js 中var flushBatchedUpdates = function () { // ReactUpdatesFlushTransactions wrappers will clear the dirtyComponents // asapEnqueued 為提前執行回調,暫不分析 while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } if (asapEnqueued) { } }};

  • 但是,仔細看上面的過程,把組件放入 dirtyComponents 後,事務結束馬上就執行 close 方法進行了處理了,和之前理解的流程好像不太一致?這時候再回頭看mountComponent和updateComponent,它們的參數:@param {ReactReconcileTransaction} transaction,也就是說整個過程都在ReactReconcileTransaction事務中(事件回調同理),自然在其中的生命周期調用setState不用引起重複 render,只會將 state 放入隊列和將組件放入 dirtyComponents 中,然後在結束後統一處理
  • ReactReconcileTransaction中 initialize 用於清空回調隊列;close 用於觸發回調函數 componentDidMount、componentDidUpdate 執行
  • 我開始一直比較疑惑的是ReactDefaultBatchingStrategy.batchedUpdates中的ReactDefaultBatchingStrategyTransaction和ReactReconcileTransaction到底是什麼關係?我試圖找出兩個 transaction 中 wrapper 是否有 merge 的情況,發現沒有。目前大概的理解和結論是這樣的:整個生命周期就是一個 transaction,即對應ReactDefaultBatchingStrategy.batchedUpdates,而ReactReconcileTransaction粒度較小,負責單個組件(所以也能看到,前者直接 new,而後者利用了對象池)。通過各自 wrapper 可以看到,前者([FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES])負責了全部組件更新 和 callback,後者([SELECTION_RESTORATION, EVENT_SUPPRESSION, ON_DOM_READY_QUEUEING)負責了各自組件自身的問題,如 focus 等。
  • 例證:ReactDom 中調用render(插入過程),實際最終調用了 ReactMount 的_renderNewRootComponent,其中執行了ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);(注意出現了batchedUpdates),而batchedMountComponentIntoNode中調用了ReactUpdates.ReactReconcileTransaction.getPooled,這樣,嵌套關係就聯繫起來了
  • 例證: ReactEventListener 的dispatchEvent,會調用ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); 和上述同理
  • 熟悉 React 生命周期的同學一定對父子組件各生命周期的執行順序很清晰(比如 componentWillMount 是從父到子),以上述的理論,是如何保證的么?上文中可以看到,FLUSH_BATCHED_UPDATES的 close方法利調用了runBatchedUpdates,來看這個方法(ReactUpdates.js):

function runBatchedUpdates(transaction) { var len = transaction.dirtyComponentsLength; // reconcile them before their children by sorting the array. dirtyComponents.sort(mountOrderComparator); // Any updates enqueued while reconciling must be performed after this entire // batch. Otherwise, if dirtyComponents is [A, B] where A has children B and // C, B could update twice in a single batch if Cs render enqueues an update // to B (since B would have already updated, we should skip it, and the only // way we can know to do so is by checking the batch counter). updateBatchNumber++; for (var i = 0; i < len; i++) { // If a component is unmounted before pending changes apply, it will still // be here, but we assume that it has cleared its _pendingCallbacks and // that was is a noop. var component = dirtyComponents[i]; // If performUpdateIfNecessary happens to enqueue any new updates, we // shouldnt execute the callbacks until the next render happens, so // stash the callbacks first var callbacks = component._pendingCallbacks; component._pendingCallbacks = null; ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber); if (callbacks) { for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance()); } } }}function mountOrderComparator(c1, c2) { return c1._mountOrder - c2._mountOrder;}

  • flushBatchedUpdates在事務ReactUpdatesFlushTransaction中,此事務是對ReactReconcileTransaction和CallbackQueue的封裝,結束時置空 dirtyComponents 並通知回調
  • performUpdateIfNecessary最終會調用updateComponent,進行更新

diff 演算法

  • 傳統對於樹的 diff 演算法,時間複雜度要達到 o(n^3),這對於用戶端顯然是不能接受的。而 React 基於幾個基礎假設,將時間複雜度優化為 o(n)
  • 假設(策略)
    • Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計
    • 擁有相同類的兩個組件將會生成相似的樹形結構,擁有不同類的兩個組件將會生成不同的樹形結構
    • 對於同一層級的一組子節點,它們可以通過唯一 id 進行區分
  • 場景
    • tree diff: 只對比同層級節點(注意前文中所有代碼中,都是只比較prevRenderedElement和nextRenderedElement)
    • component diff: 如果類型相同則繼續比較,如果類型不同則直接卸載再掛載,即上文中提到的shouldUpdateReactComponent(雖然當兩個 component 是不同類型但結構相似時,React diff 會影響性能,但正如 React 官方博客所言:不同類型的 component 是很少存在相似 DOM tree 的機會,因此為這種極端情況而做太多比較是不值得的)
    • element diff: 當一組節點處於同一層級時,React 對於每個節點提供了三種操作,分別為INSERT_MARKUP(插入)、 MOVE_EXISTING(移動)、 REMOVE_NODE(刪除)
    • 上文的代碼中,除了關心 type,還關心 key,這也是 diff 演算法的關鍵,如圖

    • 首先對新集合的節點進行循環遍歷,for (name in nextChildren),如果存在相同節點,則進行操作,是否移動是通過比較 child._mountIndex < lastIndex,符合則進行節點移動操作(即在老集合中的位置和 lastIndex 比較),lastIndex 表示訪問過的節點在老集合中最右的位置(即最大的位置)。這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置,如果新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠後,則該節點不會影響其他節點的位置,因此不用添加到差異隊列中,即不執行移動操作,只有當訪問的節點比 lastIndex 小時,才需要進行移動操作。來看具體過程:
      • 從新集合中取得 B,判斷老集合中存在相同節點 B,通過對比節點位置判斷是否進行移動操作,B 在老集合中的位置 B._mountIndex = 1,此時 lastIndex = 0,不滿足 child._mountIndex < lastIndex 的條件,因此不對 B 進行移動操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),其中 prevChild._mountIndex 表示 B 在老集合中的位置,則 lastIndex = 1,並將 B 的位置更新為新集合中的位置prevChild._mountIndex = nextIndex,此時新集合中 B._mountIndex = 0,nextIndex++ 進入下一個節點的判斷
      • 從新集合中取得 A,判斷老集合中存在相同節點 A,通過對比節點位置判斷是否進行移動操作,A 在老集合中的位置 A._mountIndex = 0,此時 lastIndex = 1,滿足 child._mountIndex < lastIndex的條件,因此對 A 進行移動操作 enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其實就是 nextIndex,表示 A 需要移動到的位置;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),則 lastIndex = 1,並將 A 的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中A._mountIndex = 1,nextIndex++ 進入下一個節點的判斷。
      • 從新集合中取得 D,判斷老集合中存在相同節點 D,通過對比節點位置判斷是否進行移動操作,D 在老集合中的位置 D._mountIndex = 3,此時 lastIndex = 1,不滿足 child._mountIndex < lastIndex的條件,因此不對 D 進行移動操作;更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),則 lastIndex = 3,並將 D 的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中D._mountIndex = 2,nextIndex++ 進入下一個節點的判斷。
      • 從新集合中取得 C,判斷老集合中存在相同節點 C,通過對比節點位置判斷是否進行移動操作,C 在老集合中的位置 C._mountIndex = 2,此時 lastIndex = 3,滿足 child._mountIndex < lastIndex 的條件,因此對 C 進行移動操作 enqueueMove(this, child._mountIndex, toIndex);更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),則 lastIndex = 3,並將 C 的位置更新為新集合中的位置 prevChild._mountIndex = nextIndex,此時新集合中 C._mountIndex = 3,nextIndex++ 進入下一個節點的判斷,由於 C 已經是最後一個節點,因此 diff 到此完成。
      • 當有新的 Component 插入時,邏輯一致,不做具體分析了
      • 當完成集合中所有節點 diff,還需要遍歷老集合,如果存在新集合中沒有但老集合中有的節點,則刪除
  • 代碼(ReactMultiChild.js),針對 element diff(tree diff 和 component diff 在之前的代碼中已經提到過):

_updateChildren: function (nextNestedChildrenElements, transaction, context) { var prevChildren = this._renderedChildren; var removedNodes = {}; var mountImages = []; var nextChildren = this._reconcilerUpdateChildren(prevChildren, nextNestedChildrenElements, mountImages, removedNodes, transaction, context); if (!nextChildren && !prevChildren) { return; } var updates = null; var name; // `nextIndex` will increment for each child in `nextChildren`, but // `lastIndex` will be the last index visited in `prevChildren`. var nextIndex = 0; var lastIndex = 0; // `nextMountIndex` will increment for each newly mounted child. var nextMountIndex = 0; var lastPlacedNode = null; for (name in nextChildren) { if (!nextChildren.hasOwnProperty(name)) { continue; } var prevChild = prevChildren && prevChildren[name]; var nextChild = nextChildren[name]; if (prevChild === nextChild) { updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex)); lastIndex = Math.max(prevChild._mountIndex, lastIndex); prevChild._mountIndex = nextIndex; } else { if (prevChild) { // Update `lastIndex` before `_mountIndex` gets unset by unmounting. lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child. } // The child must be instantiated before its mounted. updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context)); nextMountIndex++; } nextIndex++; lastPlacedNode = ReactReconciler.getHostNode(nextChild); } // Remove children that are no longer present. for (name in removedNodes) { if (removedNodes.hasOwnProperty(name)) { updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name])); } } if (updates) { processQueue(this, updates); } this._renderedChildren = nextChildren; },

綜上,在開發中,保持穩定的結構有助於性能提升,當有一組節點時,除了要設置 key,也要避免將靠後的節點移動到靠前的位置

一些其他的點

interface(ReactClass.js)

var ReactClassInterface = { mixins: DEFINE_MANY, statics: DEFINE_MANY, propTypes: DEFINE_MANY, contextTypes: DEFINE_MANY, childContextTypes: DEFINE_MANY, // ==== Definition methods ==== getDefaultProps: DEFINE_MANY_MERGED, getInitialState: DEFINE_MANY_MERGED, getChildContext: DEFINE_MANY_MERGED, render: DEFINE_ONCE, // ==== Delegate methods ==== componentWillMount: DEFINE_MANY, componentDidMount: DEFINE_MANY, componentWillReceiveProps: DEFINE_MANY, shouldComponentUpdate: DEFINE_ONCE, componentWillUpdate: DEFINE_MANY, componentDidUpdate: DEFINE_MANY, componentWillUnmount: DEFINE_MANY, // ==== Advanced methods ==== updateComponent: OVERRIDE_BASE};function validateMethodOverride(isAlreadyDefined, name) { var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null; // Disallow overriding of base class methods unless explicitly allowed. if (ReactClassMixin.hasOwnProperty(name)) { !(specPolicy === OVERRIDE_BASE) ? process.env.NODE_ENV !== production ? invariant(false, ReactClassInterface: You are attempting to override `%s` from your class specification. Ensure that your method names do not overlap with React methods., name) : _prodInvariant(73, name) : void 0; } // Disallow defining methods more than once unless explicitly allowed. if (isAlreadyDefined) { !(specPolicy === DEFINE_MANY || specPolicy === DEFINE_MANY_MERGED) ? process.env.NODE_ENV !== production ? invariant(false, ReactClassInterface: You are attempting to define `%s` on your component more than once. This conflict may be due to a mixin., name) : _prodInvariant(74, name) : void 0; }}

可以看到,和後端中interface(或是抽象類)還是有區別的,但是可以起到規範和檢查的作用,實際項目中可以借鑒

參考資料

  • facebook.github.io/reac
  • my.csdn.net/u013510838
  • zhuanlan.zhihu.com/p/20

推薦閱讀:

redux單向數據流的好處是什麼?
[上海] 招前端,移動端,小程序開發(多圖)
問一個react更新State的問題?
10 個打造 React.js App 的最佳 UI 框架
React+Redux打造「NEWS EARLY」單頁應用 一步步讓你理解最前沿技術棧的真諦

TAG:React | 源码阅读 |