我讀《深刻淺出React技術棧》之setState源碼解析

最近雙11雙12各類需求交雜在一塊兒,忙得不可開交,近期好不容易空了一些下來,讀完了 《深刻淺出React技術棧》,這本書的內容和書名一模一樣,重點在於介紹使用React過程當中相關的一些技術點,例如函數式編程、Redux、React核心的diff算法的思想等相關東西,東西仍是蠻多的,適合想要一窺react技術棧全貌的同窗,因此此次寫一下本身讀完這本書的思考和部分精華內容摘記。

this.setState

this.setState相信是你們在寫React時寫的最多的代碼,但這裏面到底都發生了什麼?爲何setState能夠是異步的?React是如何實現異步的呢?爲何不能在componentWillUpdateshouldComponetUpdate中調用setState。讓咱們來一探究竟。html

在弄清楚setState以前,首先咱們要知道的是React的生命週期,示意圖以下:
生命週期react

這裏咱們能夠注意到,爲何在componentWillUpdateshouldComponentUpdate 中是沒有見到setState的身影呢?git

首先咱們看下setState的源碼:
setState源碼github

這其中該方法傳入兩個參數partialState是新的state值,callBack後者是回調函數,updater是在構造函數中定義的一個變量,從方法名enqueueSetState中咱們能夠明白,傳入的新的state被enqueue推入了一個棧中,並非當即更新,隨後咱們繼續跟蹤代碼。
enqueueSetState算法

getInternalInstanceReadyForUpdate方法獲取了當前組件對象,並將其賦給internalInstance。接下來判斷當前組件對象的state是否存在更新隊列,若存在則把新的state值push到隊列中,若不存在,則建立一個空的新隊列。
enquueUpdate
這裏的代碼也很好理解,首先ensureInjected方法檢查當前運行的代碼是否處在一個事務(reconcile transaction)中,若不是則會拋出錯誤。且若batchingStrategy.isBatchingUpdates爲false(能夠簡單理解爲當前不是在一個批處理流程中),則進行batchedUpdates(批量更新),若爲true,則推入dirtyComponents中,接下來咱們跟蹤並看下batchingStrategy的源碼。
batchedUpdates編程

至於爲何要作batchUpdates(批量更新),用React本身的話來講,是「爲了不組件被沒必要要地更新」,並且在這裏咱們能夠看到,React更新組件是有一套本身的規則,經過組件的狀態來執行,查閱資料後得知,React內部存在着"狀態機"這個概念,也就是說當組件處於不一樣的狀態時,所執行的邏輯是不一樣的。具體來講,React以事務+狀態的方法來對組件進行更新,那麼,到底什麼是事務呢?數組

事務

下面這張圖來源於React官方源碼對事務的解釋:
事務
從圖上咱們看到,其實Transaction事務說白了就是在不改變原有方法的基礎上,在執行方法的先後進行額外的操做。具體來講,就是一個方法會被wrapper包裹,且方法須要經過perform來調用,且在被包裹方法的先後分別執行initializeclose。下面咱們看代碼,舉例說明普通函數和被wrapper包裹的函數執行時有什麼不一樣:app

function test(){
    console.log('test')
};
transaction.perform(test);
//執行initialize方法
//輸出'test'
//執行close方法

這裏可能有的人會有疑問,爲何React要引入Transaction事務這個概念呢?其實Transaction事務這個概念來源於面向切面編程,舉個簡單例子,有時候咱們在作真正的業務以前,常常須要進行驗證,受權,或者輸出日誌的操做,也就是在主要的邏輯代碼以前或者以後插入一些代碼,但咱們又不但願對原有的代碼作侵入,這時候就是Transaction發揮做用的時刻了。
對於React來講,主要有如下幾個應用場景(文字翻譯自React源碼):異步

  1. 在Reconciliation調和以前/以後保留輸入選擇範圍。 即便出現意外錯誤也能夠恢復這個選擇。
  2. 在從新排列DOM時停用事件,同時確保過後事件能被從新激活。

說了這麼多關於事務的事兒,接下來讓咱們看下ReactDefaultBatchingStrategy中的transaction是如何實現的
transaction函數式編程

咱們能夠看到這裏定義了2個wrapper,其中RESET_BATCHED_UPDATES負責在close階段重置ReactDefaultBatchingStrategyisBatchingUpdatesfalse。而FLUSH_BATCHED_UPDATES負責在close執行flushBatchedUpdates,在這個方法裏包含了Virtual DOM到真實DOM的映射等其餘操做,且此方法會清空dirtyComponents數組並執行runBatchedUpdate方法
runBatchedUpdates

咱們看到這裏dirtyComponents數組會進行一個排序操做,這裏由於一般狀況下,父組件更新後,子組件也會隨之更新,因此這裏進先進行排序,使得子組件在父組件以前被更新,同時將setState中傳入的回調函數存入callbackQueue隊列中,且performUpdateIfNecessary方法中執行了updateComponent方法,讓咱們看看這個方法都作了什麼。
updateComponents

接下來咱們重點看下這個_processPendingState方法:
_processPendingState

這個函數對state的作法就比較簡明扼要了,它主要作了如下幾件事:

  1. 若是更新隊列爲null,那麼返回原來的state
  2. 若是更新隊列有一個更新值,那麼返回更新值;
  3. 若是更新隊列有多個更新,那麼經過for循環將它們合併;

也就是說,在一個生命週期全部的state變化都會被合併,並統一處理。接下來咱們看看performUpdate作了什麼,這個函數的功能其實也簡單,就是在更新組件先後分貝執行componentWillUpdatecomponentDidUpdate。而在負責更新的_updateRenderedComponent函數中,咱們根據傳入的新舊組件信息判斷是否進行更新。若返回值爲true,執行舊組件的更新,不然的話執行舊組件的卸載和新組件的掛載。

整個流程圖以下:
總體流程

看完了這個,那麼對於開頭的「爲何不能在componentWillUpdateshouldComponetUpdate中調用setState」問題咱們就能夠進行解釋了

也就是說,組件更新時,state值尚未合併,則this._pendingStateQueuetrue,使得setState會再次調用updateComponent,隨後繼續調用componentWillUpdateshouldComponetUpdate方法,致使死循環,而正常狀況下,已經更新過的組件不會進入再次更新的流程。
performUpdateIfNecessary

看完了這些,那麼咱們再看一道經典的關於setState的題目:

class Test extends Component {
  state = { val: 0 }

componentDidMount() {
    this.setState({ val: this.state.val + 1 }); 
    console.log(this.state.val); 
    
    this.setState({ val: this.state.val + 1 }); 
    console.log(this.state.val); 
    
    setTimeout(() => {
      this.setState({ val: this.state.val + 1 }); 
      console.log(this.state.val); 
      
      this.setState({ val: this.state.val + 1 }); 
      console.log(this.state.val); 
    }, 0);
  }

  render() {
    return null;
 }
}

這道題的輸出是:

0
0
2
3

這裏簡單來講,前2個setState處於一個事務中,因此不會當即更新,而是作了合併,因此前2次log都是0,而當setTimeout被執行時,由於主線程執行完畢,已經完成了一次事務,此時是不會觸發事務狀態的,因此這時就是調用一次setState就更新一次狀態。

這也就解釋了爲何React文檔中既沒有說setState是同步更新或者是異步更新,它只是說setState並不保證同步更新。這裏引用一下React的核心成員Dan Abramov的一個回答來繼續作一點引伸。
extra

謝謝你們。:)

引用

  1. React中的事務
  2. React技術內幕之setState的祕密
  3. React源碼解析
  4. React之高階組件
相關文章
相關標籤/搜索