研究 setState
這個問題來源於一個疑惑:使用 redux 的時候 dispatch
一個 action
,為什麼可以導致檢視的更新?
首先的猜想是 store
改變後,redux 在某處呼叫了 setState
,通知了 react。
看了下程式碼發現確實如此,呼叫 dispatch action
會觸發一個 onStateChange
的函式 (這個函式在 connect
的時候就被註冊到 store
了, store
被 reducer
修改後觸發),onStateChange
函式判斷如果需要 shouldComponentUpdate
的話則執行 this.setState({})
來觸發 react 更新。
那麼問題來了:
- 為什麼
setState
可以讓檢視更新,它是如何一步步到virtualDOM
然後渲染的呢 setState
為什麼有時表現是非同步的有時又是同步的?- 為什麼在生命週期函式中,
willReceiveProps
裡可以setState
而willUpdate
不行?
捋了一下流程得出下圖,圖中每個流程塊冒號前即為被執行的函式:
簡要的說一下流程:
setState
後將傳入的state
放入佇列queue
,enqueueUpdate
方法會根據isBatchingUpdate
標誌位判斷,若當前已經在更新元件則將直接當前元件放入dirtyComponents
陣列,否則將isBatchingUpdate
置為 true 並開啟一個 "批量更新 (batchedUpdates
)" 的事務(transaction
)。
簡單地說,一個所謂的
Transaction
就是將需要執行的method
使用wrapper
封裝起來,再通過Transaction
提供的perform
方法執行。而在perform
之前,先執行所有wrapper
中的initialize
方法;perform
完成之後(即method
執行後)再執行所有的close
方法。一組initialize
及close
方法稱為一個wrapper,
Transaction
支援多個wrapper
疊加。
事務開啟後會依次執行 initialize、perform、close
方法。可以看到,batchedUpdates
在 perform
階段會再次執行 enqueueUpdate
方法,由於這時的 isBatchingUpdate
已經是 true 了所以會將當前元件放入 dirtyComponents
。關鍵就在 close
階段了,如果 dirtyComponents
為空則表示不需要更新,否則就開始更新,開啟 flushBatchedUpdates
事務。
flushBatchedUpdates
在perform
階段會將dirtyComponents
中的元件按父 > 子
元件的順序呼叫更新方法,元件在更新的時候會依次執行:
willReceiveProps -> 將 queue 中快取的 state 與快取的 state 合併 -> shouldComponentUpdate。
複製程式碼
如果判斷需要更新,則執行元件的 render
方法得到新的 reactElement
,將其與之前的 reactElement
做 diff 即可,將 diff 結果(刪除,移動等)通過 setInnerHTML
等封裝方法更新檢視即可,細節可見圖。
flushBatchedUpdates
在close
階段會再次檢查dirtyComponents
長度有沒有變化,如果變化了說明存在有新的dirtyComponent
,需要再來一次flushBatchedUpdates
。
補上 updateComponent
程式碼:
// 更新元件
updateComponent: function(transaction, prevParentElement, nextParentElement) {
var prevContext = this.context;
var prevProps = this.props;
var nextContext = prevContext;
var nextProps = prevProps;
if (prevParentElement !== nextParentElement) {
nextContext = this._processContext(nextParentElement._context);
nextProps = this._processProps(nextParentElement.props);
// 當前狀態為 RECEIVING_PROPS
this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
// 如果存在 componentWillReceiveProps,則執行
if (this.componentWillReceiveProps) {
this.componentWillReceiveProps(nextProps, nextContext);
}
}
// 設定狀態為 null,更新 state
this._compositeLifeCycleState = null;
var nextState = this._pendingState || this.state;
this._pendingState = null;
var shouldUpdate =
this._pendingForceUpdate ||
!this.shouldComponentUpdate ||
this.shouldComponentUpdate(nextProps, nextState, nextContext);
if (!shouldUpdate) {
// 如果確定元件不更新,仍然要設定 props 和 state
this._currentElement = nextParentElement;
this.props = nextProps;
this.state = nextState;
this.context = nextContext;
this._owner = nextParentElement._owner;
return;
}
this._pendingForceUpdate = false;
......
// 如果存在 componentWillUpdate,則觸發
if (this.componentWillUpdate) {
this.componentWillUpdate(nextProps, nextState, nextContext);
}
// render 遞迴渲染
var nextMarkup = this._renderedComponent.mountComponent(
thisID,
transaction,
this._mountDepth + 1
);
// 如果存在 componentDidUpdate,則觸發
if (this.componentDidUpdate) {
transaction.getReactMountReady().enqueue(
this.componentDidUpdate.bind(this, prevProps, prevState, prevContext),
this
);
}
},
複製程式碼
捋完整個流程可以回答之前一些疑惑:
- 為什麼
setState
後緊接著打 log,有時state
沒有立刻變,有時候又變了?
生命週期中的 setState
處於一個大的 transaction
中,此時的 isBatchingUpdate
為 true
,執行 setState
只會讓 dirtyComponents
陣列 push 當前元件而不會進一步處理,此時 log 來看的話 state
還是沒有變的。而如果在 transaction 之外,例如 setTimeout
裡 setState
,此時 isBatchingUpdate
為 false
,會一路直接執行下來更改 state
,所以此時 log 出來 state
是被立刻改變了的。因此 setState 不保證是同步,而不是說它一定是非同步。
2. 都在同一個 tranaction
中,為什麼在 willReceiveProps
時還可以 setState
,而在 shouldComponentUpdate
和 willUpdate
的時候 setState
會導致瀏覽器死迴圈?
元件內部有一標誌位 _compositeLifeCycleState
表示當前生命週期狀態,在 willReceiveProps
前被設定為 RECEIVING_PROPS
,在 willReceiveProps
執行後被設定為 null,而 performUpdateIfNecessary
函式在當前狀態為 MOUNTING
或 RECEIVING_PROPS
時不會繼續呼叫 updateComponent
函式。
performUpdateIfNecessary: function(transaction) {
var compositeLifeCycleState = this._compositeLifeCycleState;
// ■■■■■■■■重點■■■■■■■■■■■■
// 當狀態為 MOUNTING 或 RECEIVING_PROPS 時,則不更新
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
return;
}
var prevElement = this._currentElement;
var nextElement = prevElement;
if (this._pendingElement != null) {
nextElement = this._pendingElement;
this._pendingElement = null;
}
// 呼叫 updateComponent
this.updateComponent(
transaction,
prevElement,
nextElement
);
}
複製程式碼
因此在 willReceiveProps
時 setState
由於 _compositeLifeCycleState
已經是 RECEIVING_PROPS
了,不回觸發新的 updateComponent
,而在 willUpdate
的時候 _compositeLifeCycleState
已經被置回 null 了,因此會引發下一次的 updateComponent
,然後就再次觸發元件的各生命週期,當然也會免不了執行 willUpdate
,因此進入了死迴圈。