在我的另一篇文章 憑什么說virtual DOM是React的精髓所在 中提到過猫牡,react的性能優(yōu)化践宴,要歸功于批量DOM處理和Diff算法旧找。關(guān)于Diff算法的文章回俐,各平臺一抓一大把苍蔬,有興趣的同學可以自行查閱诱建。今天,我們就React的批量DOM處理碟绑,從一個小例子聊起俺猿,來探索一下setState 之后,究竟發(fā)生了什么格仲。
拋磚引玉
我們先來看一個簡單的例子押袍,十秒鐘思考然后確定控制臺彈出了什么:
var Test = React.createClass({
getInitialState: function(){
return {value: 'Hello Mangee'}
},
handleChangeValue: function(){
this.setState({
value: 'Goodbye Mangee'
});
console.log('newValue is', this.state.value);
},
render: function(){
return <button onClick={this.handleChangeValue}>changeValue</button>;
}
})
ReactDOM.render(
<Test />,
document.getElementById('example')
);
看完這個例子,大多數(shù)人都會自然而然認為控制臺彈出了 “newValue is Goodbye Mangee”凯肋,但事與愿違谊惭,控制臺實際上是彈出了原先的值—— “newValue is Hello Mangee”。
這是為什么呢侮东?我們來看看官方對于 setState 的一段注解:
Sets a subset of the state. Always use this to mutate
state. You should treat this.state as immutable.
There is no guarantee that this.state will be immediately updated, so
accessing this.state after calling this method may return the old value.
There is no guarantee that calls to setState will run synchronously,
as they may eventually be batched together. You can provide an optional
callback that will be executed when the call to setState is actually
completed.
從這段話中可以得知圈盔,setState對state的更新是異步的,原因正是為了實現(xiàn)我們的文首提及的批量DOM處理悄雅。
于是我們可以得到這樣一條信息:依靠 setState 的異步性驱敲,React在一段時間間隔內(nèi),將所有DOM更新收集起來宽闲,然后批量處理众眨。因此,學習 setState 的異步模型容诬,也有助于你對 React 性能優(yōu)化策略的進一步了解娩梨。
異步模型解剖
由于 React 源碼使用了大量的繼承和依賴注入,部分對象的方法需要依據(jù)依賴或繼承關(guān)系一層層追溯览徒,這里我不做逐步分析狈定,想要深入了解的同學可以自行研究。
那么接下來吱殉,就跟隨筆者的腳步掸冤,通過源碼來探尋一下從 setState 到 state 改變的完整過程。
在此之前友雳,你需要準備好 React 的兩個包稿湿,React 和 ReactDOM。
npm install react
npm install react-dom
從 setState 說起
```
this.setState({});
```
當執(zhí)行到 setState 時押赊,我們需要來找找 setState 是在哪定義的饺藤,以此來探尋 setState 后的第一步。
// react/ReactComponent
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
我們發(fā)現(xiàn)流礁,setState 調(diào)用了組件本身的 updater 對象的兩個方法enqueueSetState 和 enqueueCallback涕俗,其中,callback 是更新完成后執(zhí)行的回調(diào)函數(shù)神帅。
updater(更新器)再姑,每個 React 組件都擁有的、用于驅(qū)動 state 更新的工具對象找御,按照繼承依賴關(guān)系元镀,可以追溯到 updater 的本體,即react-dom/ReactUpdateQueue霎桅,其中定義了我們所要找的 enqueueSetState 和 enqueueCallback 兩個方法栖疑。
那么擇其一,enqueueSetState 里邊滔驶,又發(fā)生了什么遇革?
// react-dom/ReactUpdateQueue
enqueueSetState: function (publicInstance, partialState) {
// 獲得 internalInstance 實例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 將 partialState 推入實例自身的 _pendingStateQueue (狀態(tài)隊列)等候更新
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// 驅(qū)動更新
enqueueUpdate(internalInstance);
}
enqueueCallback 的實現(xiàn)步驟跟以上一樣,最終結(jié)果也是將回調(diào)函數(shù) callback 推入回調(diào)隊列揭糕,等待執(zhí)行萝快。
到目前,待更新的 state 已經(jīng)在狀態(tài)隊列里候著了著角,什么時候拿出來更新呢杠巡?這就得來繼續(xù)看看 enqueueUpdate 這個函數(shù)了。
// react-dom/ReactUpdateQueue
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
原來是執(zhí)行了 ReactUpdates 模塊的 enqueueUpdate 方法雇寇,讓我們把頻道切換到 react-dom/ReactUpdates氢拥。
// react-dom/ReactUpdates
function enqueueUpdate(component) {
// 若 batchingStrategy 不處于批量更新收集狀態(tài),則 batchedUpdates 所有隊列中的更新
// 值得注意的是锨侯,此時傳入的 component 將不參加當前批的更新嫩海,而是作為下一批進行更新
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 若 batchingStrategy 處于批量更新收集狀態(tài),則把 component 推進 dirtyComponents 數(shù)組等待更新
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
這里囚痴,batchingStrategy 對象是作為一個批處理的管理者叁怪,依照指定的批量策略,對到來的一系列 component 更新做分批深滚。
設(shè)想一個場景:我們?nèi)ネ孢^山車時奕谭,管理員會分批安排游客進場涣觉,等這一批游客玩完之后,再安排下一批進場血柳,而在當前批游客正在玩的過程中官册,有游客到來,都需要先排隊难捌。
在這里膝宁,component 就是游客,batchingStrategy 就是管理員根吁,isBatchingUpdates 標志就是有沒有游客正在玩员淫。當一批DOM處理完成后,調(diào)用 batchedUpdates 方法击敌,更新下一批 dirtyComponents介返。
有些人可能會有疑問,為什么這里感覺像是開了兩個線程沃斤,一個在完成“排隊”映皆,一個在完成“批處理”。實際上不是的轰枝,js是單線程的捅彻,所以當一個event loop內(nèi)陸陸續(xù)續(xù)有新的 component 更新驅(qū)動來到這里時,都會被阻塞在 dirtyComponents 中鞍陨,等到全部收集完畢步淹,才進行批處理,不存在邊處理邊排隊的情況诚撵。
另外缭裆,值得注意的是,batchingStrategy 對象是通過 injection 方法注入的寿烟,經(jīng)過一番艱難追溯之后澈驼,發(fā)現(xiàn)了 batchingStrategy 就是 ReactDefaultBatchingStrategy。讓我們看看這個模塊調(diào)用 batchedUpdates 方法之后筛武,發(fā)生了什么缝其。
// react-dom/ReactDefaultBatchingStrategy
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
可以看出,ReactDefaultBatchingStrategy 對象十分簡潔徘六,isBatchingUpdates 是批收集判斷的標志位内边,batchedUpdates 方法用于發(fā)動一個批處理。在其中我們可以發(fā)現(xiàn)待锈,isBatchingUpdates 標志位就是在 batchedUpdates 發(fā)起的時候置為 true 的漠其。那 isBatchingUpdates 又是在哪里復(fù)位為 false 的呢?這就得引出一個React 框架設(shè)計的核心概念——Transaction (事務(wù))。
隨處可見的Transaction
Transaction(事務(wù))是一個針對函數(shù)執(zhí)行的包裝(wrapper)和屎,React關(guān)于 Transaction 的源碼中拴驮,出現(xiàn)了這樣一幅有趣而形象的圖:
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
從上圖可知,Transaction 實例 transaction 在創(chuàng)建的時候向自身注入 wrapper柴信,實現(xiàn)的效果是套啤,通過 transaction.perform 執(zhí)行的函數(shù) anyMethod,先執(zhí)行 transaction 的所有 initialize 方法颠印,再執(zhí)行 anyMethod纲岭,執(zhí)行完再執(zhí)行所有的 close 方法抹竹。引用來自 React 源碼剖析系列 - 解密 setState 的一個簡單的例子說明:
// react-dom/Transaction
var Transaction = require('./Transaction');
// 我們自己定義的 Transaction
var MyTransaction = function() {
// do sth.
};
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('before method perform');
},
close: function() {
console.log('after method perform');
}
}];
};
});
var transaction = new MyTransaction();
var testMethod = function() {
console.log('test');
}
transaction.perform(testMethod);
// before method perform
// test
// after method perform
基于此线罕,我們回過頭來看看,ReactDefaultBatchingStrategy.batchedUpdates 執(zhí)行后窃判,發(fā)生了什么钞楼。
// react-dom/ReactDefaultBatchingStrategy
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
batchedUpdates 方法中,transaction 是 ReactDefaultBatchingStrategyTransaction 的實例袄琳,也是一類事務(wù)询件,perform 方法傳入的 callback 正是我們前邊探究過的、用于做DOM批收集的 enqueueUpdate 函數(shù)∷舴現(xiàn)在讓我們把注意力轉(zhuǎn)移到它的 initialize 和 close 方法上:
// react-dom/ReactDefaultBatchingStrategy
// 定義復(fù)位 wrapper
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
};
// 定義批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
}
_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
}
});
initialize 方法是兩個空函數(shù)宛琅,我們不關(guān)注,close 方法逗旁,按照順序嘿辟,將在 enqueueUpdate 執(zhí)行結(jié)束后,先把 isBatchingUpdates 復(fù)位片效,再發(fā)起一個 DOM 的批更新红伦。到這里我們恍然大悟,所謂的批處理淀衣,實際上是明確地分為了批收集和批更新兩個步驟昙读,而上邊所有的內(nèi)容,都只是在完成批收集這個環(huán)節(jié)膨桥。
React 對于這個核心環(huán)節(jié)的設(shè)計可是一點都不含糊蛮浑,所以懵逼了的同學請翻回去重新來一遍,還沒吐的同學請堅持只嚣。
批更新
整理一下妝容陵吸,我們繼續(xù)來看看這后續(xù)的批更新環(huán)節(jié)是如何實現(xiàn)的。
對于批更新這部分介牙,涉及到關(guān)于 React 從 virtual DOM 向真實 DOM 反饋的許多細節(jié)考慮壮虫,一來筆者未能完全滲透,二來與本文所要探究的問題無關(guān),因此接下來貼出的源碼是經(jīng)過大量刪減的囚似,只保留了我們需要關(guān)注的部分剩拢。
銜接批收集的最后一步,ReactUpdates 模塊調(diào)用了 flushBatchedUpdates 方法饶唤。
// react-dom/ReactUpdates
var flushBatchedUpdates = function () {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
// 創(chuàng)建一個 ReactUpdatesFlushTransaction 實例
var transaction = ReactUpdatesFlushTransaction.getPooled();
// 調(diào)用實例的 perform 方法進行更新
transaction.perform(runBatchedUpdates, null, transaction);
// 釋放實例徐伐,回歸實例池
ReactUpdatesFlushTransaction.release(transaction);
}
}
};
核心步驟又出現(xiàn)了另外一個 transaction,它執(zhí)行了一個 runBatchedUpdates 函數(shù)募狂。當然办素,老規(guī)矩,遇到 transaction祸穷,查查它的 initialize 和 close 方法是很必要的性穿,但由于 runBatchedUpdates 執(zhí)行的調(diào)用棧比較深入,要講的略多雷滚,所以我們放到 runBatchedUpdates 執(zhí)行完畢再來看需曾。先關(guān)注 runBatchedUpdates 完成了哪些:
// react-dom/ReactUpdates
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 排序,保證 dirtyComponent 從父級到子級的 render 順序
dirtyComponents.sort(mountOrderComparator);
updateBatchNumber++;
// 遍歷 dirtyComponents
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
// 取到該 dirtyComponent 的回調(diào)數(shù)組
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
// 更新該 dirtyComponent
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);
// 當存在 callbacks 時祈远,將 callbacks 逐項提取呆万,推入 transaction.callbackQueue
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
}
}
}
}
遍歷 dirtyComponents 數(shù)組,并且利用一個新模塊 ReactReconciler 的
performUpdateIfNecessary 方法將 dirtyComponent 逐個更新车份。
讓我們來看看 ReactReconciler.performUpdateIfNecessary 完成了什么:
// react-dom/ReactReconciler
performUpdateIfNecessary: function (internalInstance, transaction, updateBatchNumber) {
internalInstance.performUpdateIfNecessary(transaction);
}
調(diào)用了組件的 performUpdateIfNecessary 方法谋减,而又一番艱苦追溯,我們發(fā)現(xiàn)扫沼,組件為 ReactCompositeComponent 的實例出爹,因而也在 ReactCompositeComponent 中發(fā)現(xiàn)了關(guān)于它的定義:
// react-dom/ReactReconciler
updateComponent: function (transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext) {
var inst = this._instance;
var nextContext = inst.context;
var nextProps = nextParentElement.props;
``` // 對 comtext 和 props 的一系列校驗
// 關(guān)注的核心
var nextState = this._processPendingState(nextProps, nextContext);
``` // 拿到更新后的 nextState 進行反饋到真實 DOM 上的更新
}
最終,整個過程算是繞了一圈充甚,調(diào)用了組件上的 _processPendingState 方法以政,在這個方法中,我們終于完成了對 state 的合并更新:
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
// 將該組件狀態(tài)隊列里所有的 state 更新統(tǒng)一處理合并
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
}
咦伴找,說好的回調(diào)函數(shù)會在更新完成后調(diào)用的呢盈蛮?
別急,不是還漏了前文提到的那個 transaction 的 close 方法沒瞧瞧嘛:
var NESTED_UPDATES = {
initialize: function () {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function () {
// 移除已遍歷過的 dirtyComponents
if (this.dirtyComponentsLength !== dirtyComponents.length) {
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0;
}
}
};
var UPDATE_QUEUEING = {
initialize: function () {
this.callbackQueue.reset();
},
close: function () {
// 完成更新后執(zhí)行 callbackQueue 的回調(diào)函數(shù)
this.callbackQueue.notifyAll();
}
};
var TRANSACTION_WRAPPERS = [NESTED_UPDATES, UPDATE_QUEUEING];
function ReactUpdatesFlushTransaction() {
}
_assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
}
}
看技矮,配合得真完美抖誉,不出所料,正是利用了 transaction 的 close 方法衰倦,將一開始緩存在 callbacks 隊列中的回調(diào)函數(shù)袒炉,逐一取出并執(zhí)行,這里我就不做展開了樊零。
捋一捋思路
經(jīng)過了這樣一系列復(fù)雜而深入的調(diào)用我磁,setState 終于完成了 state 的合并更新孽文。但其實,我所提取的只是 setState 的一個通用過程夺艰,文首拋出的例子芋哭,其實早在 click 事件觸發(fā)的那一刻起,就已經(jīng)執(zhí)行了一個 batchedUpdates郁副,因此等執(zhí)行到 setState 的時候减牺,已經(jīng)置身于一個大的 transaction 中,其調(diào)用棧已經(jīng)非常深入了存谎。但是篇幅限制拔疚,也因筆者能力有限,故而放棄對 react 完整的事件觸發(fā)機制進行深入探討既荚,這里就大致地還原一下setState的異步流機制稚失,給看到這里還沒崩潰的同學,總結(jié)一下吧:
1固以、click事件觸發(fā)墩虹;
2嘱巾、React 內(nèi)置事件監(jiān)聽器啟動一個事務(wù)(transaction) 憨琳,把批策略(ReactDefaultBatchingStrategy)的批收集標志位置為 true;
3旬昭、在事務(wù)的 perform 中篙螟,setState發(fā)起;
4问拘、觸發(fā)更新器(updater)上的 enqueueSetState 和 enqueueCallback遍略,把 state 和 callback 推入等待隊列,并且驅(qū)動 enqueueUpdate 更新骤坐;
5绪杏、觸發(fā) batchingStrategy 的 batchedUpdates 方法,啟動一個事務(wù)纽绍,進行批收集蕾久;
6、收集完成后拌夏,觸發(fā)事務(wù)的 close 方法僧著,復(fù)位標志位,并執(zhí)行批處理障簿;
7盹愚、觸發(fā) ReactUpdates 的 flushBatchedUpdates 方法,啟動另外一個事務(wù)站故,執(zhí)行一系列的調(diào)用最終完成更新皆怕;
8、更新完成后,觸發(fā)事務(wù)的 close 方法愈腾,調(diào)用隊列里的回調(diào)函數(shù)朗兵;
9、最外層的事務(wù)完成顶滩,釋放調(diào)用棧余掖。
關(guān)于 setState 的異步模型解析就到這里,學藝不精礁鲁,恐有錯漏盐欺,歡迎吐槽!
參考文獻如下仅醇,極力推薦:
拆解setState[一][一源看世界][之React]
拆解setState[二][一源看世界][之React]
拆解setState[三][一源看世界][之React]
setState 之后發(fā)生了什么 —— 淺談 React 中的 Transaction
React 源碼剖析系列 - 解密 setState
React源碼分析5 — setState機制