最近雙11雙12各類需求交雜在一塊兒,忙得不可開交,近期好不容易空了一些下來,讀完了 《深刻淺出React技術棧》,這本書的內容和書名一模一樣,重點在於介紹使用React過程當中相關的一些技術點,例如函數式編程、Redux、React核心的diff算法的思想等相關東西,東西仍是蠻多的,適合想要一窺react技術棧全貌的同窗,因此此次寫一下本身讀完這本書的思考和部分精華內容摘記。
this.setState
相信是你們在寫React時寫的最多的代碼,但這裏面到底都發生了什麼?爲何setState能夠是異步的?React是如何實現異步的呢?爲何不能在componentWillUpdate
和shouldComponetUpdate
中調用setState
。讓咱們來一探究竟。html
在弄清楚setState以前,首先咱們要知道的是React的生命週期,示意圖以下:react
這裏咱們能夠注意到,爲何在componentWillUpdate
和shouldComponentUpdate
中是沒有見到setState
的身影呢?git
首先咱們看下setState
的源碼:github
這其中該方法傳入兩個參數partialState
是新的state值,callBack
後者是回調函數,updater
是在構造函數中定義的一個變量,從方法名enqueueSetState
中咱們能夠明白,傳入的新的state被enqueue
推入了一個棧中,並非當即更新,隨後咱們繼續跟蹤代碼。算法
getInternalInstanceReadyForUpdate
方法獲取了當前組件對象,並將其賦給internalInstance
。接下來判斷當前組件對象的state
是否存在更新隊列,若存在則把新的state
值push到隊列中,若不存在,則建立一個空的新隊列。
這裏的代碼也很好理解,首先ensureInjected
方法檢查當前運行的代碼是否處在一個事務(reconcile transaction)中,若不是則會拋出錯誤。且若batchingStrategy.isBatchingUpdates
爲false(能夠簡單理解爲當前不是在一個批處理流程中),則進行batchedUpdates
(批量更新),若爲true,則推入dirtyComponents中,接下來咱們跟蹤並看下batchingStrategy的源碼。編程
至於爲何要作batchUpdates
(批量更新),用React本身的話來講,是「爲了不組件被沒必要要地更新」,並且在這裏咱們能夠看到,React更新組件是有一套本身的規則,經過組件的狀態來執行,查閱資料後得知,React內部存在着"狀態機"這個概念,也就是說當組件處於不一樣的狀態時,所執行的邏輯是不一樣的。具體來講,React以事務+狀態的方法來對組件進行更新,那麼,到底什麼是事務呢?數組
下面這張圖來源於React官方源碼對事務的解釋:
從圖上咱們看到,其實Transaction事務說白了就是在不改變原有方法的基礎上,在執行方法的先後進行額外的操做。具體來講,就是一個方法會被wrapper
包裹,且方法須要經過perform
來調用,且在被包裹方法的先後分別執行initialize
和close
。下面咱們看代碼,舉例說明普通函數和被wrapper
包裹的函數執行時有什麼不一樣:app
function test(){ console.log('test') }; transaction.perform(test); //執行initialize方法 //輸出'test' //執行close方法
這裏可能有的人會有疑問,爲何React要引入Transaction事務這個概念呢?其實Transaction事務這個概念來源於面向切面編程,舉個簡單例子,有時候咱們在作真正的業務以前,常常須要進行驗證,受權,或者輸出日誌的操做,也就是在主要的邏輯代碼以前或者以後插入一些代碼,但咱們又不但願對原有的代碼作侵入,這時候就是Transaction發揮做用的時刻了。
對於React來講,主要有如下幾個應用場景(文字翻譯自React源碼):異步
- 在Reconciliation調和以前/以後保留輸入選擇範圍。 即便出現意外錯誤也能夠恢復這個選擇。
- 在從新排列DOM時停用事件,同時確保過後事件能被從新激活。
說了這麼多關於事務的事兒,接下來讓咱們看下ReactDefaultBatchingStrategy
中的transaction
是如何實現的函數式編程
咱們能夠看到這裏定義了2個wrapper,其中RESET_BATCHED_UPDATES
負責在close
階段重置ReactDefaultBatchingStrategy
的isBatchingUpdates
爲false
。而FLUSH_BATCHED_UPDATES
負責在close
執行flushBatchedUpdates
,在這個方法裏包含了Virtual DOM到真實DOM的映射等其餘操做,且此方法會清空dirtyComponents
數組並執行runBatchedUpdate
方法
咱們看到這裏dirtyComponents
數組會進行一個排序操做,這裏由於一般狀況下,父組件更新後,子組件也會隨之更新,因此這裏進先進行排序,使得子組件在父組件以前被更新,同時將setState中傳入的回調函數存入callbackQueue
隊列中,且performUpdateIfNecessary
方法中執行了updateComponent
方法,讓咱們看看這個方法都作了什麼。
接下來咱們重點看下這個_processPendingState
方法:
這個函數對state的作法就比較簡明扼要了,它主要作了如下幾件事:
null
,那麼返回原來的state
;也就是說,在一個生命週期全部的state
變化都會被合併,並統一處理。接下來咱們看看performUpdate作了什麼,這個函數的功能其實也簡單,就是在更新組件先後分貝執行componentWillUpdate
和componentDidUpdate
。而在負責更新的_updateRenderedComponent
函數中,咱們根據傳入的新舊組件信息判斷是否進行更新。若返回值爲true,執行舊組件的更新,不然的話執行舊組件的卸載和新組件的掛載。
整個流程圖以下:
看完了這個,那麼對於開頭的「爲何不能在componentWillUpdate
和shouldComponetUpdate
中調用setState
」問題咱們就能夠進行解釋了
也就是說,組件更新時,state值尚未合併,則this._pendingStateQueue
爲true
,使得setState會再次調用updateComponent,隨後繼續調用componentWillUpdate
和shouldComponetUpdate
方法,致使死循環,而正常狀況下,已經更新過的組件不會進入再次更新的流程。
看完了這些,那麼咱們再看一道經典的關於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的一個回答來繼續作一點引伸。
謝謝你們。:)