加入新團隊後,團隊項目使用了React Native。剛開始接觸React Native,除了學習React Native的使用,更要了解React.js這個框架,才能更好的使用。而React框架中,筆者一開始就感受奇妙的,就是這個看似同步,表現卻不必定是同步的setState方法。看了網上一些文章,結論彷佛都是從某幾篇博客相互借鑑的結論,但裏面筆者仍是以爲有一些不太明白的地方,幸好React.js源碼是開源的。順着源碼看下去,一些困惑的問題終於有些眉目。javascript
開始以前,讀者可思考如下幾個問題:java
如下源碼基於咱們團隊在用的React 16.0.0版本,目前最新的React 16.4.0版本的類名和文件結構均有很大變化,但設計思想應該仍是差很少的,可供參考。react
setState的最上層天然在ReactBaseClasses中。git
//ReactBaseClasses.js
ReactComponent.prototype.setState = function(partialState, callback) {
// ...
//調用內部updater
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼
而這個updater,在初始化時已經告訴咱們,是實際使用時注入的。github
//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方法的內容。app
//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.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會在執行後執行。
回到剛剛的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應該是同步的呀?是哪裏出了問題呢。
別急,還記得更新策略裏面那個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沒有被立刻更新。
在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:以響應式的思路使用。