React原始碼解析(三):詳解事務與更新佇列

ssssyoki發表於2017-11-15

在前兩篇文章中,我們分析了React元件的實現,掛載以及生命週期的流程。在閱讀原始碼的過程中,我們經常會看到諸如transactionUpdateQueue這樣的程式碼,這涉及到React中的兩個概念:事務和更新佇列。因為之前的文章對於這些我們一筆帶過,所以本篇我們基於大家都再熟悉不過的setState方法來探究事務機制和更新佇列。

1.setState相關

在第一篇文章《React原始碼解析(一):元件的實現與掛載》中我們已經知道,通過class宣告的元件原型具有setState方法:

React原始碼解析(三):詳解事務與更新佇列

該方法傳入兩個引數partialStatecallBack,前者是新的state值,後者是回撥函式。而updater是在建構函式中進行定義的:

React原始碼解析(三):詳解事務與更新佇列

可以看出updater是建構函式傳入的,所以找到哪裡執行了new ReactComponent,就能找到updater是什麼。以自定義元件ReactCompositeComponent為例,在_constructComponentWithoutOwner方法中,我們發現了它的蹤跡:

return new Component(publicProps, publicContext, updateQueue);
複製程式碼

對應引數發現updater其實就是updateQueue。接下來我們看看this.updater.enqueueSetState中的enqueueSetState是什麼:

React原始碼解析(三):詳解事務與更新佇列

getInternalInstanceReadyForUpdate方法的目的是獲取當前元件物件,將其賦值給internalInstance變數。接下來判斷當前元件物件的state更新佇列是否存在,如果存在則將partialState也就是新的state值加入佇列;如果不存在,則建立該物件的更新佇列,可以注意到佇列是以陣列形式存在的。我們看下最後呼叫的enqueueUpdate方法做了哪些事:

React原始碼解析(三):詳解事務與更新佇列

由程式碼可見,當batchingStrategy.isBatchingUpdatesfalse時,將執行batchedUpdates更新佇列,若為true時,則將元件放入dirtyComponent中。我們看下batchingStrategy的原始碼:

React原始碼解析(三):詳解事務與更新佇列

大致地看下,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呼叫,在被包裹方法前後分別執行initializeclose。舉例說明普通函式和被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是如何定義的:

React原始碼解析(三):詳解事務與更新佇列

從上面的思維導圖可知,isBatchingUpdates初始值為false,當以事務的形式執行transaction.perform(enqueueUpdate)時,實際上執行流程如下:

// RESET_BATCHED_UPDATES.initialize() 實際為空函式
// enqueue()
// RESET_BATCHED_UPDATES.close()
複製程式碼

用流程圖來說明:

React原始碼解析(三):詳解事務與更新佇列

用文字說明的話,那就是RESET_BATCHED_UPDATES這個wrapper的作用是設定isBatchingUpdates也就是元件更新狀態的值,元件有更新要求的話則設定為更新狀態,更新結束後重新恢復原狀態。

這樣做有什麼好處呢?當然是為了避免元件的重複render,提升效能啦~

RESET_BATCHED_UPDATES是用於更改isBatchingUpdates的布林值false或者true,那FLUSH_BATCHED_UPDATES的作用是什麼呢?其實可以大致猜到它的作用是更新元件,先看下FLUSH_BATCHED_UPDATES.close()的實現邏輯:

React原始碼解析(三):詳解事務與更新佇列

可以看到flushBatchedUpdates方法迴圈遍歷所有的dirtyComponents,又通過事務的形式呼叫runBatchedUpdates方法,因為原始碼較長所以在這裡直接說明該方法所做的兩件事:

  • 一是通過執行updateComponent方法來更新元件
  • 二是若setState方法傳入了回撥函式則將回撥函式存入callbackQueue佇列。

看下updateComponent原始碼:

React原始碼解析(三):詳解事務與更新佇列

可以看到執行了componentWillReceiveProps方法和shouldComponentUpdate方法。其中不能忽視的一點是在shouldComponentUpdate之前,執行了_processPendingState方法,我們看下這個函式做了什麼:

React原始碼解析(三):詳解事務與更新佇列

該函式主要對state進行處理:
1.如果更新佇列為null,那麼返回原來的state
2.如果更新佇列有一個更新,那麼返回更新值;
3.如果更新佇列有多個更新,那麼通過for迴圈將它們合併;
綜上說明了,在一個生命週期內,在componentShouldUpdate執行之前,所有的state變化都會被合併,最後統一處理。

回到_updateComponent,最後如果shouldUpdatetrue,執行_performComponentUpdate方法:

React原始碼解析(三):詳解事務與更新佇列

大致瀏覽下會發現還是同樣的套路,執行componentWillUpdate生命週期方法,更新完成後執行componentDidUpdate方法。我們看下負責更新的_updateRenderedComponent方法:

React原始碼解析(三):詳解事務與更新佇列

這段程式碼的思路就很清晰了:

  1. 獲取舊的元件資訊
  2. 獲取新的元件資訊
  3. shouldUpdateReactComponent是一個方法(下文簡稱should函式),根據傳入的新舊元件資訊判斷是否進行更新。
  4. should函式返回true,執行舊元件的更新。
  5. should函式返回false,執行舊元件的解除安裝和新元件的掛載。

結合前面的流程圖,我們對整個元件更新流程進行補充:

React原始碼解析(三):詳解事務與更新佇列

4.寫在最後

(1)setState回撥函式

setState回撥函式與state的流程相似,stateenqueueSetState處理,回撥函式由enqueueCallback處理,感興趣的讀者可以自行探究。

(2)關於setState導致的崩潰問題

我們已經知道,this.setState實際呼叫了enqueueSetState,在元件更新時,因為新的state還未進行合併處理,故在下面performUpdateIfNecessary程式碼中this._pendingStateQueuetrue

React原始碼解析(三):詳解事務與更新佇列

而合併state後React會會將this._pendingStateQueue設定為null,這樣dirtyComponent進入下一次批量處理時,已經更新過的元件不會進入重複的流程,保證元件只做一次更新操作。

所以不能在componentWillUpdate中呼叫setState的原因,就是setState會令_pendingStateQueuetrue,導致再次執行updateComponent,而後會再次呼叫componentWillUpdate,最終迴圈呼叫componentWillUpdate導致瀏覽器的崩潰。

(3)關於React依賴注入

我們在之前的程式碼中,對於更新佇列的標誌batchingStrategy,我們直接轉向對ReactDefaultBatchingStrategy進行分析,這是因為React內部存在大量的依賴注入。在React初始化時,ReactDefaultInjection.js注入到ReactUpdates中作為預設的strategy。依賴注入在React的服務端渲染中有大量的應用,有興趣的同學可以自行探索。

回顧:
《React原始碼解析(一):元件的實現與掛載》
《React原始碼解析(二):元件的生命週期》
《React原始碼解析(四):事件系統》
聯絡郵箱:ssssyoki@foxmail.com

相關文章