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 values
if (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 doesn't 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 it's 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` didn't throw, we don't 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
:負責事件的儲存,具體儲存在listenerBankPlugin: 根據不同的事件型別,構造不同的合成事件,可以連線原生事件和元件
當事件觸發時,會呼叫
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 it's 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 () {
// ReactUpdatesFlushTransaction's 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 C's 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
// shouldn't 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 it's 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
(或是抽象類)還是有區別的,但是可以起到規範和檢查的作用,實際專案中可以借鑑
相關文章
- React Fiber 原始碼解析React原始碼
- react——js原始碼解析ReactJS原始碼
- React原始碼解析(二):react-elementReact原始碼
- React原始碼解析(三):react-componentReact原始碼
- react-redux原始碼解析ReactRedux原始碼
- React-Router 原始碼解析React原始碼
- React Hooks 原始碼解析(譯)ReactHook原始碼
- React Hook原始碼解析(二)ReactHook原始碼
- React Hooks原始碼深度解析ReactHook原始碼
- react-loadable 原始碼解析React原始碼
- redux && react-redux原始碼解析ReduxReact原始碼
- React-原始碼解析-DOM模型React原始碼模型
- React Hooks 原始碼解析(3):useStateReactHook原始碼
- 【原始碼解析】React Native元件渲染原始碼React Native元件
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- React原始碼解析(一):JSX到javascriptReact原始碼JSJavaScript
- React-Redux 原始碼解析 一(createStore)ReactRedux原始碼
- 深度解析 create-react-app 原始碼ReactAPP原始碼
- create-react-app 原始碼解析之react-scriptsReactAPP原始碼
- React 原始碼解析系列 - React 的 render 階段(三):completeUnitOfWorkReact原始碼
- React 原始碼解析系列 - React 的 render 階段(二):beginWorkReact原始碼
- React原始碼解析(2):元件的掛載React原始碼元件
- 5000字的React-native原始碼解析React原始碼
- React原始碼解析(1):jsx語法是如何解析React原始碼JS
- React原始碼解析(3):元件的生命週期React原始碼元件
- React-原始碼解析-setState執行機制React原始碼
- React 原始碼解析系列 - React 的 render 異常處理機制React原始碼
- React-原始碼解析-生命週期(自定義元件)React原始碼元件
- React 原始碼分析React原始碼
- React原始碼分析React原始碼
- Java Timer原始碼解析(定時器原始碼解析)Java原始碼定時器
- 【原始碼解析】- ArrayList原始碼解析,絕對詳細原始碼
- React原始碼解析之Commit第一子階段「before mutation」React原始碼MIT
- react原始碼淺析(四):react-isReact原始碼
- ReactNative原始碼解析-初識原始碼React原始碼
- Koa 原始碼解析原始碼
- Koa原始碼解析原始碼
- RxPermission原始碼解析原始碼