加入新團隊後,團隊專案使用了React Native。剛開始接觸React Native,除了學習React Native的使用,更要了解React.js這個框架,才能更好的使用。而React框架中,筆者一開始就感覺奇妙的,就是這個看似同步,表現卻不一定是同步的setState方法。看了網上一些文章,結論似乎都是從某幾篇部落格相互借鑑的結論,但裡面筆者還是覺得有一些不太明白的地方,幸虧React.js原始碼是開源的。順著原始碼看下去,一些困惑的問題終於有些眉目。
開始之前,讀者可思考以下幾個問題:
- setState是非同步還是同步的
- 在setTimeout方法中呼叫setState的值為何馬上就能更新
- setState中傳入一個Function為何值馬上就能更新
- setState為何要如此設計
- setState的最佳實踐是什麼
以下原始碼基於我們團隊在用的React 16.0.0版本,目前最新的React 16.4.0版本的類名和檔案結構均有很大變化,但設計思想應該還是差不多的,可供參考。
setState的入口
setState的最上層自然在ReactBaseClasses中。
//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
// ...
//呼叫內部updater
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製程式碼
而這個updater,在初始化時已經告訴我們,是實際使用時注入的。
//ReactBaseClasses.js
function ReactComponent(props, context, updater) {
// ...
// 真正的updater在renderer注入
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
複製程式碼
找到注入的地方,目的是找到updater是個什麼型別。
//ReactCompositeComponet.js
mountComponent: function(
transaction,
hostParent,
hostContainerInfo,
context,
) {
// ...
// 由Transaction獲取,這個是ReactReconcileTransaction
var updateQueue = transaction.getUpdateQueue();
// ...
inst.updater = updateQueue;
// ...
}
複製程式碼
//ReactReconcileTransaction.js
getUpdateQueue: function() {
return ReactUpdateQueue;
},
複製程式碼
終於看到了具體enqueSetState方法的內容。
//ReactUpdateQueue.js
enqueueSetState: function(
publicInstance,
partialState,
callback,
callerName,
) {
// ...
// 外部示例轉化為內部例項
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
// ...
// 將需要更新的state放入等待佇列中
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
// ...
// callback也一樣放入等待佇列中
if (callback !== null) {
// ...
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
}
enqueueUpdate(internalInstance);
},
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
複製程式碼
而更新操作由ReactUpdates這個類負責。
//ReactUpdates.js
function enqueueUpdate(component) {
//...
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
//...
}
複製程式碼
而這個isBatchingUpdates的判斷,就是代表是否在批量更新中。如果正在更新中,則整個元件放入dirtyComponents陣列中,後面會講到。這裡這個batchingStrategy,其實就是ReactDefaultBatchingStrategy(外部注入的)。
//ReactDOMStackInjection.js
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
複製程式碼
而這個類裡的,則會讓掛起更新狀態,並呼叫transaction的perform。
//ReactDefaultBatchingStrategy.js
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// 正常情況不會走入if中
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
},
};
複製程式碼
Transaction
這裡簡單解釋下事務(Transaction)的概念,先看原始碼中對事務的一張解釋圖。
//Transaction.js
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
複製程式碼
簡單來說,事務相當於對某個方法(anyMethod)執行前和執行後的多組鉤子的集合。
可以方便的在某個方法前後分別做一些事情,而且可以分wrapper定義,一個Wrapper一對鉤子。
具體來說,可以在Wrapper裡定義initialize和close方法,initialize會在anyMethod執行前執行,close會在執行後執行。
更新策略裡的Transaction
回到剛剛的batchedUpdates方法,裡面那個transaction其實執行前都是空方法,而callback是外界傳入的enqueueUpdate方法本身,也就是說,執行時會被isBatchingUpdates卡住進入加入dirtyCompoments中。之後就會執行close方法裡面去改變isBatchingUpdates的值和執行flushBatchedUpdates方法。
//ReactDefaultBatchingStrategy.js
// 更新狀態isBatchingUpdates的wrapper
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
},
};
// 真正更新狀態的wrapper
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
// 兩個wrapper
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
// 新增transaction的wrappers
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
複製程式碼
而這個flushBatchedUpdates方法,按照dirtyComonents裡的數量,每次執行了一個transaction。
//ReactUpdates.js
var flushBatchedUpdates = function() {
while (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
};
複製程式碼
而這個transaction的執行前後前後的鉤子如下。
//ReactUpdtes.js
// 開始時同步dirComponents數量,結束時通過檢查是否在執行中間runBatchedUpdates方法時還有新加入的component,有的話就重新執行一遍
var NESTED_UPDATES = {
initialize: function() {
this.dirtyComponentsLength = dirtyComponents.length;
},
close: function() {
if (this.dirtyComponentsLength !== dirtyComponents.length) {
dirtyComponents.splice(0, this.dirtyComponentsLength);
flushBatchedUpdates();
} else {
dirtyComponents.length = 0;
}
},
};
var TRANSACTION_WRAPPERS = [NESTED_UPDATES];
//新增wrapper
Object.assign(ReactUpdatesFlushTransaction.prototype, Transaction, {
getTransactionWrappers: function() {
return TRANSACTION_WRAPPERS;
},
});
複製程式碼
所以真正更新方法應該在runBatchedUpdates中。
//ReactUpdates.js
function runBatchedUpdates(transaction) {
// 排序,保證父元件比子元件先更新
dirtyComponents.sort(mountOrderComparator);
// ...
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
//這裡開始進入更新元件的方法
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
}
}
複製程式碼
而ReactReconciler中的performUpateIfNecessary方法只是一個殼。
performUpdateIfNecessary: function(
internalInstance,
transaction,
updateBatchNumber,
) {
// ...
internalInstance.performUpdateIfNecessary(transaction);
// ...
},
複製程式碼
而真正的方法在ReactCompositeComponent中,如果等待佇列中有該更新的state,那麼就呼叫updateComponent。
//ReactCompositeComponent.js
performUpdateIfNecessary: function(transaction) {
if (this._pendingElement != null) {
// ...
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(
transaction,
this._currentElement,
this._currentElement,
this._context,
this._context,
);
} else {
// ...
}
},
複製程式碼
這個方法判斷了做了一些判斷,而我們也看到了nextState的值才是最後被更新給state的值。
//ReactCompositeComponent.js
updateComponent: function(
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext,
) {
// 這個將排序佇列裡的state合併到nextState
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true;
if (shouldUpdate) {
//...
} else {
// 這裡才正式更新state
//...
inst.state = nextState;
//...
}
}
複製程式碼
這個方法也解釋了為什麼傳入函式的state會更新。
_processPendingState: function(props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
//更新了就可以置空了
this._pendingStateQueue = null;
var nextState = replace ? queue[0] : inst.state;
var dontMutate = true;
for (var i = replace ? 1 : 0; i < queue.length; i++) {
//如果setState傳入是函式,那麼接收的state是上輪更新過的state
var partial = queue[i];
let partialState = typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial;
if (partialState) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
return nextState;
},
複製程式碼
好像setState是同步的耶
而如果按照這個流程看完,setState應該是同步的呀?是哪裡出了問題呢。
別急,還記得更新策略裡面那個Transaction麼。那裡中間呼叫的callback是外層傳入的,也就說有可能還有其它呼叫了batchedUpdates呢。那麼也就是說,中間的callback,並不止setState會引起。在程式碼裡搜尋後發現,果真還有幾處呼叫了batchedUpdates方法。
比如ReactMount的這兩個方法
//ReactMount.js
_renderNewRootComponent: function(
nextElement,
container,
shouldReuseMarkup,
context,
callback,
) {
// ...
// The initial render is synchronous but any updates that happen during
// rendering, in componentWillMount or componentDidMount, will be batched
// according to the current batching strategy.
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context,
);
// ...
},
unmountComponentAtNode: function(container) {
// ...
ReactUpdates.batchedUpdates(
unmountComponentFromNode,
prevComponent,
container,
);
return true;
// ...
},
複製程式碼
比如ReactDOMEventListener
//ReactDOMEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {
// ...
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
ReactGenericBatching.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
// ...
}
},
//ReactDOMStackInjection.js
ReactGenericBatching.injection.injectStackBatchedUpdates(
ReactUpdates.batchedUpdates,
);
複製程式碼
所以比如在componentDidMount中直接呼叫時,ReactMount.js 中的**_renderNewRootComponent** 方法已經呼叫了,也就是說,整個將 React 元件渲染到 DOM 中的過程就處於一個大的 Transaction 中,而其中的callback沒有馬上被執行,那麼自然state沒有被馬上更新。
setState為什麼這麼設計
在react中,state代表UI的狀態,也就是UI由state改變而改變,也就是UI=function(state)。筆者覺得,這體現了一種響應式的思想,而響應式與命令式的不同,在於命令式著重看如何命令的過程,而響應式看中資料變化如何輸出。而React中對Rerender做出的努力,對渲染的優化,響應式的setState設計,其實也是其中搭配而不可少的一環。
最後
最前面的問題,相信每個人都有自己的答案,我這裡給出我自己的理解。
Q:setState是非同步還是同步的?
A:同步的,但有時候是非同步的表現。
Q:在setTimeout方法中呼叫setState的值為何馬上就能更新?
A:因為本身就是同步的,也沒有別的因素阻塞。
Q:setState中傳入一個Function為何值馬上就能更新?
A:原始碼中的策略。
Q:setState為何要如此設計?
A:為了以響應式的方式改變UI。
Q:setState的最佳實踐是什麼?
A:以響應式的思路使用。