這是個人剖析 React 源碼的第五篇文章。這篇文章開始將會帶着你們學習組件更新過程相關的內容,儘量的脫離源碼來了解原理,下降你們的學習難度。前端
文章分爲三部分,在這部分的文章中你能夠學習到以下內容:node
在另外的兩篇文章中你能夠學習到如何調和組件及渲染組件的過程。react
想必你們都知道大部分狀況下屢次 setState
不會觸發屢次渲染,而且 state
的值也不是實時的,這樣的作法可以減小沒必要要的性能消耗。git
handleClick () { // 初始化 `count` 爲 0 console.log(this.state.count) // -> 0 this.setState({ count: this.state.count + 1 }) this.setState({ count: this.state.count + 1 }) console.log(this.state.count) // -> 0 this.setState({ count: this.state.count + 1 }) console.log(this.state.count) // -> 0 } 複製代碼
那麼這個行爲是如何實現的呢?答案是批量更新。接下來咱們就來學習批量更新是如何實現的。github
其實這個背後的原理至關之簡單。假如 handleClick
是經過點擊事件觸發的,那麼 handleClick
其實差很少會被包裝成這樣:算法
isBatchingUpdates = true try { handleClick() } finally { isBatchingUpdates = false // 而後去更新 } 複製代碼
在執行 handleClick
以前,其實 React 就會默認此次觸發事件的過程當中若是有 setState
的話就應該批量更新。瀏覽器
當咱們在 handleClick
內部執行 setState
時,更新狀態的這部分代碼首先會被丟進一個隊列中等待後續的使用。而後繼續處理更新的邏輯,畢竟觸發 setState
確定會觸發一系列組件更新的流程。可是在這個流程中若是 React 發現須要批量更新 state
的話,就會當即中斷更新流程。markdown
也就是說,雖然咱們在 handleClick
中調用了三次 setState
,可是並不會走完三次的組件更新流程,只是把更新狀態的邏輯丟到了一個隊列中。當 handleClick
執行完畢以後會再執行一次組件更新的流程。數據結構
另外組件更新流程實際上是有兩個大相徑庭的分支的。一種就是觸發更新之後一次完成所有的組件更新流程;另外一種是觸發更新之後分時間片斷完成全部的組件更新,用戶體驗更好,這種方式被稱之爲任務調度。若是你想詳細瞭解這一塊的內容,能夠閱讀我以前 寫的文章。ide
固然本文也會說起一部分調度相關的內容,畢竟這塊也包含在組件更新流程中。可是在學習任務調度以前,咱們須要先來學習下 fiber 相關的內容,由於這塊內容是 React 實現各類這些新功能的基石。
在瞭解 Fiber 以前,咱們先來了解下爲何 React 官方要費那麼大勁去重構 React。
在 React 15 版本的時候,咱們若是有組件須要更新的話,那麼就會遞歸向下遍歷整個虛擬 DOM 樹來判斷須要更新的地方。這種遞歸的方式弊端在於沒法中斷,必須更新完全部組件纔會中止。這樣的弊端會形成若是咱們須要更新一些龐大的組件,那麼在更新的過程當中可能就會長時間阻塞主線程,從而形成用戶的交互、動畫的更新等等都不能及時響應。
React 的組件更新過程簡而言之就是在持續調用函數的一個過程,這樣的一個過程會造成一個虛擬的調用棧。假如咱們控制這個調用棧的執行,把整個更新任務拆解開來,儘量地將更新任務放到瀏覽器空閒的時候去執行,那麼就能解決以上的問題。
那麼如今是時候介紹 Fiber 了。Fiber 從新實現了 React 的核心算法,帶來了殺手鐗增量更新功能。它有能力將整個更新任務拆分爲一個個小的任務,而且能控制這些任務的執行。
這些功能主要是經過兩個核心的技術來實現的:
在前文中咱們說到了須要拆分更新任務,那麼如何把控這個拆分的顆粒度呢?答案是 fiber。
咱們能夠把每一個 fiber 認爲是一個工做單元,執行更新任務的整個流程(不包括渲染)就是在反覆尋找工做單元並運行它們,這樣的方式就實現了拆分任務的功能。
拆分紅工做單元的目的就是爲了讓咱們能控制 stack frame(調用棧中的內容),能夠隨時隨地去執行它們。由此使得咱們在每運行一個工做單元后均可以按狀況繼續執行或者中斷工做(中斷的決定權在於調度算法)。
那麼 fiber 這個數據結構到底長什麼樣呢?如今就讓咱們來一窺究竟。
fiber 內部其實存儲了不少上下文信息,咱們能夠把它認爲是改進版的虛擬 DOM,它一樣也對應了組件實例及 DOM 元素。同時 fiber 也會組成 fiber tree,可是它的結構再也不是一個樹形,而是一個鏈表的結構。
如下是 fiber 中的一些重要屬性:
{ ... // 瀏覽器環境下指 DOM 節點 stateNode: any, // 造成列表結構 return: Fiber | null, child: Fiber | null, sibling: Fiber | null, // 更新相關 pendingProps: any, // 新的 props memoizedProps: any, // 舊的 props // 存儲 setState 中的第一個參數 updateQueue: UpdateQueue<any> | null, memoizedState: any, // 舊的 state // 調度相關 expirationTime: ExpirationTime, // 任務過時時間 // 大部分狀況下每一個 fiber 都有一個替身 fiber // 在更新過程當中,全部的操做都會在替身上完成,當渲染完成後, // 替身會代替自己 alternate: Fiber | null, // 先簡單認爲是更新 DOM 相關的內容 effectTag: SideEffectTag, // 指這個節點須要進行的 DOM 操做 // 如下三個屬性也會造成一個鏈表 nextEffect: Fiber | null, // 下一個須要進行 DOM 操做的節點 firstEffect: Fiber | null, // 第一個須要進行 DOM 操做的節點 lastEffect: Fiber | null, // 最後一個須要進行 DOM 操做的節點,同時也可用於恢復任務 .... } 複製代碼
總的來講,咱們能夠認爲 fiber 就是一個工做單元的數據結構表現,固然它一樣也是調用棧中的一個重要組成部分。
Fiber 和 fiber 不是同一個概念。前者表明新的調和器,後者表明 fiber node,也能夠認爲是改進後的虛擬 DOM。
每次有新的更新任務發生的時候,調度器都會按照策略給這些任務分配一個優先級。好比說動畫的更新優先級會高點,離屏元素的更新優先級會低點。
經過這個優先級咱們能夠獲取一個該更新任務必須執行的截止時間,優先級越高那麼截止時間就越近,反之亦然。這個截止時間是用來判斷該任務是否已通過期,若是過時的話就會立刻執行該任務。
而後調度器經過實現 requestIdleCallback
函數來作到在瀏覽器空閒的時候去執行這些更新任務。
這其中的實現原理略微複雜。簡單來講,就是經過定時器的方式,來獲取每一幀的結束時間。獲得每一幀的結束時間之後咱們就能判斷當下距離結束時間的一個差值。
若是還未到結束時間,那麼也就意味着我能夠繼續執行更新任務;若是已通過告終束時間,那麼就意味着當前幀已經沒有時間給我執行任務了,必須把執行權交還給瀏覽器,也就是打斷任務的執行。
另外當開始執行更新任務(也就是尋找工做單元並執行的過程)時,若是有新的更新任務進來,那麼調度器就會按照二者的優先級大小來進行決策。若是新的任務優先級小,那麼固然繼續當下的任務;若是新的任務優先級大,那麼會打斷任務並開始新的任務。
以上就是調度器的原理簡介,若是你想了解更多的內容,能夠閱讀我以前寫的文章: 剖析 React 源碼:調度原理。
如今是時候把文章中說起到的內容整合起來了,另外咱們假設更新任務一定會觸發調度。
當交互事件調用 setState
後,會觸發批量更新,在整個交互事件回調執行完以前 state
都不會發生變動。
回調執行完畢後,開始更新任務,並觸發調度。調度器會給這些更新任務一一設置優先級,而且在瀏覽器空閒的時候去執行他們,固然任務過時除外(會馬上觸發更新,再也不等待)。
若是在執行更新任務的時候,有新的任務進來,會判斷兩個任務的優先級高低。假如新任務優先級高,那麼打斷舊的任務,從新開始,不然繼續執行任務。
閱讀源碼是一個很枯燥的過程,可是收益也是巨大的。若是你在閱讀的過程當中有任何的問題,都歡迎你在評論區與我交流。
另外寫這系列是個很耗時的工程,須要維護代碼註釋,還得把文章寫得儘可能讓讀者看懂,最後還得配上畫圖,若是你以爲文章看着還行,就請不要吝嗇你的點贊。
最後,以爲內容有幫助能夠關注下個人公衆號 「前端真好玩」咯,會有不少好東西等着你。