在前兩篇文章中,我們分析了React元件的實現,掛載以及生命週期的流程。在閱讀原始碼的過程中,我們經常會看到諸如transaction
和UpdateQueue
這樣的程式碼,這涉及到React中的兩個概念:事務和更新佇列。因為之前的文章對於這些我們一筆帶過,所以本篇我們基於大家都再熟悉不過的setState
方法來探究事務機制和更新佇列。
1.setState相關
在第一篇文章《React原始碼解析(一):元件的實現與掛載》中我們已經知道,通過class
宣告的元件原型具有setState
方法:
該方法傳入兩個引數partialState
和callBack
,前者是新的state值,後者是回撥函式。而updater
是在建構函式中進行定義的:
可以看出updater
是建構函式傳入的,所以找到哪裡執行了new ReactComponent
,就能找到updater
是什麼。以自定義元件ReactCompositeComponent
為例,在_constructComponentWithoutOwner
方法中,我們發現了它的蹤跡:
return new Component(publicProps, publicContext, updateQueue);
複製程式碼
對應引數發現updater
其實就是updateQueue
。接下來我們看看this.updater.enqueueSetState
中的enqueueSetState
是什麼:
getInternalInstanceReadyForUpdate
方法的目的是獲取當前元件物件,將其賦值給internalInstance
變數。接下來判斷當前元件物件的state更新佇列是否存在,如果存在則將partialState
也就是新的state值加入佇列;如果不存在,則建立該物件的更新佇列,可以注意到佇列是以陣列形式存在的。我們看下最後呼叫的enqueueUpdate
方法做了哪些事:
由程式碼可見,當batchingStrategy.isBatchingUpdates
為false
時,將執行batchedUpdates
更新佇列,若為true
時,則將元件放入dirtyComponent
中。我們看下batchingStrategy
的原始碼:
大致地看下,isBatchingUpdates的初始值是false
,且batchedUpdates
內部執行傳入的回撥函式。
看到這麼長的邏輯似乎有點懵,但從這些程式碼我們隱約意識到React並不是隨隨便便就進行元件的更新,而是通過狀態(好像是true/false)的判斷來執行。實際上,React內部採用了"狀態機"的概念,元件處於不同的狀態時,所執行的邏輯也並不相同。以元件更新流程為例,React以事務+狀態的形式對元件進行更新,因此接下來我們探討事務的機制。
2.transaction事務
首先看下官方原始碼的解析圖:
<pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
複製程式碼
從流程圖上看很簡單,每一個方法會被wrapper
所包裹,必須用perform
呼叫,在被包裹方法前後分別執行initialize
和close
。舉例說明普通函式和被wrapper
包裹的函式執行時有什麼不同:
function method(){
console.log('111')
};
transaction.perform(method);
//執行initialize方法
//輸出'111'
//執行close方法
複製程式碼
我們知道在前面的batchingStrategy
的程式碼中transaction.perform(callBack)
實際呼叫的是transaction.perform(enqueueUpdate)
,但enqueueUpdate
方法中仍然存在transaction.perform(enqueueUpdate)
,這樣豈不是造成了死迴圈?
為了避免可能死迴圈的問題,wrapper
的作用就顯現出來了。我們看下這兩個wrapper
是如何定義的:
從上面的思維導圖可知,isBatchingUpdates
初始值為false
,當以事務的形式執行transaction.perform(enqueueUpdate)
時,實際上執行流程如下:
// RESET_BATCHED_UPDATES.initialize() 實際為空函式
// enqueue()
// RESET_BATCHED_UPDATES.close()
複製程式碼
用流程圖來說明:
用文字說明的話,那就是RESET_BATCHED_UPDATES
這個wrapper
的作用是設定isBatchingUpdates
也就是元件更新狀態的值,元件有更新要求的話則設定為更新狀態,更新結束後重新恢復原狀態。
這樣做有什麼好處呢?當然是為了避免元件的重複render,提升效能啦~
RESET_BATCHED_UPDATES
是用於更改isBatchingUpdates
的布林值false
或者true
,那FLUSH_BATCHED_UPDATES
的作用是什麼呢?其實可以大致猜到它的作用是更新元件,先看下FLUSH_BATCHED_UPDATES.close()
的實現邏輯:
可以看到flushBatchedUpdates
方法迴圈遍歷所有的dirtyComponents
,又通過事務的形式呼叫runBatchedUpdates
方法,因為原始碼較長所以在這裡直接說明該方法所做的兩件事:
- 一是通過執行
updateComponent
方法來更新元件 - 二是若
setState
方法傳入了回撥函式則將回撥函式存入callbackQueue
佇列。
看下updateComponent
原始碼:
可以看到執行了componentWillReceiveProps
方法和shouldComponentUpdate
方法。其中不能忽視的一點是在shouldComponentUpdate
之前,執行了_processPendingState
方法,我們看下這個函式做了什麼:
該函式主要對state
進行處理:
1.如果更新佇列為null
,那麼返回原來的state
;
2.如果更新佇列有一個更新,那麼返回更新值;
3.如果更新佇列有多個更新,那麼通過for迴圈將它們合併;
綜上說明了,在一個生命週期內,在componentShouldUpdate
執行之前,所有的state
變化都會被合併,最後統一處理。
回到_updateComponent
,最後如果shouldUpdate
為true
,執行_performComponentUpdate
方法:
大致瀏覽下會發現還是同樣的套路,執行componentWillUpdate
生命週期方法,更新完成後執行componentDidUpdate
方法。我們看下負責更新的_updateRenderedComponent
方法:
這段程式碼的思路就很清晰了:
- 獲取舊的元件資訊
- 獲取新的元件資訊
shouldUpdateReactComponent
是一個方法(下文簡稱should
函式),根據傳入的新舊元件資訊判斷是否進行更新。should
函式返回true
,執行舊元件的更新。should
函式返回false
,執行舊元件的解除安裝和新元件的掛載。
結合前面的流程圖,我們對整個元件更新流程進行補充:
4.寫在最後
(1)setState
回撥函式
setState
回撥函式與state
的流程相似,state
由enqueueSetState
處理,回撥函式由enqueueCallback
處理,感興趣的讀者可以自行探究。
(2)關於setState
導致的崩潰問題
我們已經知道,this.setState
實際呼叫了enqueueSetState
,在元件更新時,因為新的state
還未進行合併處理,故在下面performUpdateIfNecessary
程式碼中this._pendingStateQueue
為true
:
而合併state
後React會會將this._pendingStateQueue
設定為null
,這樣dirtyComponent
進入下一次批量處理時,已經更新過的元件不會進入重複的流程,保證元件只做一次更新操作。
所以不能在componentWillUpdate
中呼叫setState
的原因,就是setState
會令_pendingStateQueue
為true
,導致再次執行updateComponent
,而後會再次呼叫componentWillUpdate
,最終迴圈呼叫componentWillUpdate
導致瀏覽器的崩潰。
(3)關於React依賴注入
我們在之前的程式碼中,對於更新佇列的標誌batchingStrategy
,我們直接轉向對ReactDefaultBatchingStrategy
進行分析,這是因為React內部存在大量的依賴注入。在React初始化時,ReactDefaultInjection.js
注入到ReactUpdates
中作為預設的strategy。依賴注入在React的服務端渲染中有大量的應用,有興趣的同學可以自行探索。
回顧:
《React原始碼解析(一):元件的實現與掛載》
《React原始碼解析(二):元件的生命週期》
《React原始碼解析(四):事件系統》
聯絡郵箱:ssssyoki@foxmail.com