你真的瞭解 setState 嗎?
最近為了準備面試,回顧了一下曾經做過的筆記,卻忽然發現怎麼也理不順 setState 了,尷尬。又重翻了一下《深入React技術棧》,發現其中的解密 setState 部分存在一些不準確的地方,只好又去重新看了一下 React 的原始碼(16版本之前的 stack reconciler),雖然現在已經被重構了,但還是記錄一下以便加深理解,此為背景。
這裡我們用書上的例子,知乎上也有:楊森:React 原始碼剖析系列 - 解密 setState 。
class Example extends React.Component { constructor() { super(); this.state = { val: 0 }; } componentDidMount() { this.setState({val: this.state.val + 1}); console.log(this.state.val);// 第 1 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val);// 第 2 次 log setTimeout(() => { this.setState({val: this.state.val + 1}); console.log(this.state.val);// 第 3 次 log this.setState({val: this.state.val + 1}); console.log(this.state.val);// 第 4 次 log }, 0); } render() { return null; } };
答案是 0, 0, 2, 3。很顯然,前兩次呼叫沒有立刻更新 state,而後兩次呼叫卻立刻更新了state。這是為什麼呢?首先讓我們看一下 setState 的原始碼造成這種差異最核心的部分是 enqueueUpdate 函式。
ReactComponent.prototype.setState = function(partialState, callback) { this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); } }; enqueueSetState: function(publicInstance, partialState) { // 注意,internalInstance 不是元件的例項,而是 VDOM 節點 var internalInstance = getInternalInstanceReadyForUpdate( publicInstance, 'setState', ); if (!internalInstance) { return; } // 把 partialState 放入佇列中 var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []); queue.push(partialState); enqueueUpdate(internalInstance); }, function enqueueUpdate(component) { // ... if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }
這個函式的核心是 batchingStrategy 物件,在看懂這個函式之前必須介紹一下 transaction 的概念。React 的原始碼中,有一幅圖很形象的說明了 transaction 的作用,就是用 wrapper 把函式包裹起來執行。
/* * <pre> *wrappers (injected at creation time) *++ *|| *+-----------------|--------|--------------+ *|v|| *|+---------------+|| *|+--|wrapper1|---|----+| *||+---------------+v|| *||+-------------+|| *||+----|wrapper2|--------+| *|||+-------------+||| *|||||| *|vvvv| wrapper *| +---+ +---++---------++---+ +---+ | invariants * perform(anyMethod) | || |||||| || | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> *| || |||||| || | *| || |||||| || | *| || |||||| || | *| +---+ +---++---------++---+ +---+ | *|initializeclose| *+-----------------------------------------+ * </pre> */
下面我們繼續看batchingStrategy(src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js)這個物件
var RESET_BATCHED_UPDATES = { initialize: emptyFunction, close: function() { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }, }; // 真正遍歷 dirtyComponents 執行更新任務是在這個 wrapper 的 close 函式裡 var FLUSH_BATCHED_UPDATES = { initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), }; // 批量更新事務的 wrappers var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; function ReactDefaultBatchingStrategyTransaction() { this.reinitializeTransaction(); } Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, { getTransactionWrappers: function() { return TRANSACTION_WRAPPERS; }, }); // 文中的 批量更新事務 指是就是這個 transaction var transaction = new ReactDefaultBatchingStrategyTransaction(); var ReactDefaultBatchingStrategy = { // 狀態 isBatchingUpdates: false, batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 如果已經處於更新中,直接執行 callback,否則使用批量更新事務執行 callback if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { return transaction.perform(callback, null, a, b, c, d, e); } }, };
可見是不是同步更新 state 的關鍵在於 enqueueUpdate 函式中的判斷條件
function enqueueUpdate(component) { // ... // 如果 isBatchingUpdates 為false,也就是沒在事務中,則直接開啟批量更新事務執行 enqueueUpdate if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); }
如果沒在批量更新事務中呼叫 setState,則會在 enqueueUpdate 中用事務執行 enqueueUpdate,也就是執行完 dirtyComponents.push(component) 後事務就進入 close 階段,接著就是執行flushBatchedUpdates 進行實際的元件更新,這時一次 setState 對應一次 flushBatchedUpdates。setState 的呼叫結束後,元件的 state 已經更新,整個過程並沒有用到任何非同步的 api。
如果呼叫 setState 的時候已經在批量更新事務的 method 中,則只是用把元件放入 dirtyComponents 列表中,等待批量更新事務中的 method 執行完畢後,才會執行 flushBatchedUpdates。這時多次呼叫 setState 只會執行一次 flushBatchedUpdates,也就是隻會更新一次 update。
看到這裡,setState 的邏輯已經很清晰了,所謂的同步非同步只是在 setState 的呼叫棧中是否會呼叫 flushBatchedUpdates,而會不會立刻呼叫 flushBatchedUpdates 則取決於你在呼叫
setState 的時候是不是已經處於批量更新事務中。
元件的生命週期函式和事件回撥函式本身就是在批量更新事務中執行的,因此例子中的1和2呼叫不會立刻更新 state,而3和4是在 setTimeout 回撥函式中呼叫的,此時沒有處於批量更新事務中,因此每次呼叫都會立刻更新 state。
既然已經看到了這裡,不妨在看一下 flushBatchedUpdates 的實現
// src/renderers/shared/stack/reconciler/ReactUpdates.js var flushBatchedUpdates = function() { // .. while (dirtyComponents.length || asapEnqueued) { if (dirtyComponents.length) { var transaction = ReactUpdatesFlushTransaction.getPooled(); transaction.perform(runBatchedUpdates, null, transaction); ReactUpdatesFlushTransaction.release(transaction); } } }; function runBatchedUpdates(transaction) { // .. var len = transaction.dirtyComponentsLength; dirtyComponents.sort(mountOrderComparator); updateBatchNumber++; for (var i = 0; i < len; i++) { var component = dirtyComponents[i]; ReactReconciler.performUpdateIfNecessary( component, transaction.reconcileTransaction, updateBatchNumber, ); } } // src/renderers/shared/stack/reconciler/ReactReconciler.js performUpdateIfNecessary: function( internalInstance, transaction, updateBatchNumber, ) { // .. internalInstance.performUpdateIfNecessary(transaction); } // src/renderers/shared/stack/reconciler/ReactCompositeComponent.js performUpdateIfNecessary: function(transaction) { if (this._pendingElement != null) { ReactReconciler.receiveComponent( this, this._pendingElement, transaction, this._context, ); // 這個判斷是避免多次呼叫的 setState 造成多次渲染的關鍵 } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) { this.updateComponent( transaction, this._currentElement, this._currentElement, this._context, this._context, ); } else { this._updateBatchNumber = null; } }
雖然在批量更新事務中多次呼叫 setState 還是是會多次把 component push 進 dirtyComponents 佇列,但是在第一次更新元件時就把元件的 _pendingStateQueue 清空了,所以後續的更新都不會再執行,也就避免了重複的渲染。
結論:
- setState 的實現並沒有涉及到任何的非同步 api。
- 真正更新元件 state 的是 flushBatchedUpdates 函式,而 setState 不一定會呼叫這個函式。
- setState 會不會立刻更新 state 取決於呼叫 setState 時是不是已經處於批量更新事務中。
- 元件的生命週期函式和繫結的事件回撥函式都是在批量更新事務中執行的。