React 原始碼分析-呼叫ReactDOM.render後發生了什麼
我們知道, 對於一般的React 應用, 瀏覽器會首先執行程式碼 ReactDOM.render來渲染頂層元件, 在這個過程中遞迴渲染巢狀的子元件, 最終所有元件被插入到DOM中. 我們來看看
呼叫ReactDOM.render 發生了什麼
大致過程(只展示主要的函式呼叫):
1、建立元素
首先, 對於你寫的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 }
React.createElement原始碼在ReactElement.js中, 其他邏輯比較簡單, 值得說的是props屬性, 這個props屬性裡面包含的就是我們給元件傳的各種屬性:
// jsx
return ( <div className='container'> "dscsdcsd" <i onClick={(e) => console.log(e)}>{this.state.val}</i> <Children val={this.state.val}/> </div> )
// bable 轉義後
// createElement(type, props, children)
return React.createElement(
'div', { className: 'container' },
'"dscsdcsd"',
React.createElement('i', { onClick: e => console.log(e) }, this.state.val),
React.createElement(Children, { val: this.state.val })
);
// 對應的元素樹
{
$$typeof: Symbol(react.element)
key: null
props: { // props有children屬性, 描述子元件, 同樣是元素
children: [
""dscsdcsd"",
// 子元素
{$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …},
{$$typeof: Symbol(react.element), type: class Children, props: {…}, …}
]
className: 'container'
}
ref: null
type: 'div'
_owner: null
_store: {validated: false}
_self: null
_source: null
}
2、建立對應型別的React元件
創建出來的元素被當作引數和指定的 DOM container 一起傳進ReactDOM.render. 接下來會呼叫一些內部方法, 接著呼叫了 instantiateReactComponent, 這個函式根據element的型別例項化對應的component. 當element的型別為:
- string時, 說明是文字, 建立ReactDOMTextComponent;
- ReactElement時, 說明是react元素, 進一步判斷element.type的型別, 當為
string時, 為DOM原生節點, 建立ReactDOMComponent;
函式或類時, 為react 元件, 建立ReactCompositeComponent
instantiateReactComponent函式在instantiateReactComponent.js :
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.開啟批量更新以應對可能的setState
在呼叫instantiateReactComponent拿到元件例項後, React 接著呼叫了batchingStrategy.batchedUpdates並將元件例項當作引數執行批量更新(首次渲染為批量插入).
批量更新是一種優化策略, 避免重複渲染, 在很多框架都存在這種機制. 其實現要點是要弄清楚何時儲存更新, 何時批量更新.
在React中, 批量更新受batchingStrategy控制,而這個策略除了server端都是ReactDefaultBatchingStrategy:
那麼React是如何實現批量更新的? 在ReactDefaultBatchingStrategy.js我們看到, 它的實現依靠了事務.
在 Transaction.js中, React 介紹了事務:
React 把要呼叫的函式封裝一層wrapper, 這個wrapper一般是一個物件, 裡面有initialize方法, 在呼叫函式前呼叫;有close方法, 在函式執行後呼叫. 這樣封裝的目的是為了, 在要呼叫的函式執行前後某些不變性約束條件(invariant)仍然成立.函式呼叫前後某些規則仍然成立. 比如, 在調和(reconciliation)前後保留UI元件一些狀態.React 中, 事務就像一個黑盒, 函式在這個黑盒裡被執行, 執行前後某些規則仍然成立, 即使函式報錯. 事務提供了函式執行的一個安全環境.
// 事務的抽象實現, 作為基類
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;
}
};
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 事務的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, // 是否處在一次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;
}
}