背景React15
react核心思想:
內存中維護一顆虛擬DOM樹,數據變化時(setState),自動更新虛擬DOM,得到一顆新樹,然后diff新老虛擬DOM樹,找到有變化的部分,得到一個change(patch),將這個patch加入隊列,最終批量更新這些path到DOM中。簡單說就是:diff + patch。
react 執行render()和setState()進行渲染時主要有兩個階段:
調度階段 (Reconciler): 用新數據生成一顆新樹,遍歷虛擬dom,diff新老virtual dom樹,搜集具體的UI差異,找到需要更新的元素,放到更新隊列中。
渲染階段(Renderer): 遍歷更新隊列,通過調用宿主環境的API,實際更新渲染對應元素。宿主環境,比如dom,native等。
3種實例
1.DOM, 對應真實的DOM節點
2.Element 描述UI,通過React.createElement()得到
3.Instance React維護的虛擬DOM, 根據Element創建,對組件和DOM節點的抽象表示,維護組件內部狀態和與DOM樹的關系
優化實踐
react本身為了提高頁面渲染性能,推出了一些最佳實踐
1.vdom 減少對dom的直接操作
2.無狀態組件 減少組件內部狀態和復雜度
3.shouldComponentUpdate 減少diff的次數
4.immutable 減少diff的成本
但以上都是針對js執行而提出的方法,具體到瀏覽器渲染,如何避免長時間的線程占用並沒有給出好的建議。
why Fiber的來源
Fiber之前的Reconciler階段采用的是Stack Reconciler, 其自頂向下遍歷vdom tree, 遞歸組件執行任務,過程無法中斷。
假設有一個層級很復雜的組件,在頂層組件內執行setState, 那么調用棧可能會很長。由於調用棧過長,中間可能還有一些復雜操作,這些任務無法中斷,就導致主線程被長時間阻塞。由於瀏覽器里渲染和js執行共一個主線程,在對響應要求高的場景,比如手勢,動畫等,就容易造成卡頓,延遲等現象,從而影響用戶體驗。
Fiber的出現就是為了解決這個問題。
Fiber 的解決思路:把渲染更新過程拆分成多個子任務,每次只做一小部分,做完看是否還有剩余時間,如果有繼續下一個任務;如果沒有,掛起當前任務,將時間控制權交給主線程,等主線程不忙的時候在繼續執行。
what Fiber是什么
計算機通常追蹤程序執行的方式是使用調用堆棧。執行函數時,不斷的把堆棧幀加入到堆棧中,一個堆棧幀就表示一個要執行的工作。
在處理UI時,如果執行太多的工作,就可能導致動畫丟幀。
新版本的瀏覽器實現了有助於解決這個問題的API:requestIdleCallback 和 requestAnimationFrame.
requestIdleCallback 調度在空閑期間調用的低優先級函數
requestAnimationFrame調度在下一個動畫幀上調用的高優先級函數。
問題是,為了使用這些API,就需要一種方法將渲染工作分解為增量單元。如果僅依賴於調用堆棧,它將繼續工作直到堆棧為空,無法中斷。
為了實現增量渲染的調度,就必須重新實現這個堆棧幀,以便可以將堆棧幀保留在內存中,然后按照自己的調度算法執行他們。同時由於這些堆棧棧是我們手動處理的,我們還可以加入並發或者錯誤邊界等功能。
因此Fiber就是重新實現的堆棧幀,本質上Fiber也可以理解為是一個虛擬的堆棧幀,將可中斷的任務拆分成多個子任務,通過按照優先級來自由調度子任務,分段更新,從而將之前的同步渲染改為異步渲染。
react內部有自己的優先級判斷邏輯,比如動畫,用戶交互等任務優先級就明顯要高。
Fiber的目標
- 將耗時長可中斷的任務拆分成多個子任務
- 對正在做的工作調整優先級,可以重做,復用上次的結果
Fiber特性
- 增量渲染,把一個渲染任務拆分成多個子任務,平均到多個渲染幀中執行,每次只做一小段,做完后就把時間控制權上交給主線程
- 在渲染更新時,能夠暫停,復用任務
- 不同類型的任務具有不同的優先級
- 並發方面的其他能力
- 錯誤邊界
how Fiber實現
簡單來說就是,時間分片 + 鏈表結構 。
fiber就是維護每一個分片的數據結構。
fiber && fiber tree
react中沒有明確的Virtual Dom, 可以把fiber理解為我們習慣上的虛擬Dom概念。
react在render中第一次渲染時,利用React.createElement會創建一棵Element樹,同時會利用Element中的數據創建Fiber tree。不同的Element類型對應不同類型的Fiber Node。在后續的更新過程中(setState),每次重新渲染都會重新創建Element, 但是fiber不會,fiber只會使用對應的Element中的數據來更新自己必要的屬性,
一個Fiber Node可以認為是一個對象,它表示組件需要做的工作。一個Element對應一個或多個Fiber Node。
上面提到Fiber要做增量更新,所以就要額外維護一些上下文信息,所以react 擴展出了 fiber tree 的概念,更新過程就是根據輸入的數據以及當前fiber tree,構造出新的fiber tree(workInProgress tree)。上面我們提到了Instance, Fiber基於此進行了擴展,添加了一些其他概念:
1 2 3 4 5 6 7 8 9 10 11 |
{ // 搜集diff差異結果,每個workInProgress tree節點都有一個effect list // 當前節點更新完畢,會向上merge effect list effect // reconcile過程中的快照,工作過程樹,類似於“草稿”,用戶不可見 workInProgress // fiber tree 與vdom tree類似,描述增量更新需要的上下文信息 fiber } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// 假設有一個<Card />組件 // FiberNode 結構如下: { // 定義fiber節點類型,類組件指向構造函數,dom元素指向標簽名稱 type: Card, // Fiber類型,將React Element映射成對應的Fiber類型,用於說明協調過程中需要完成的工作 // HostRoot|HostComponent|ClassComponent|FunctionComponent... tag: 1, // 不同tag代表不同類型的副作用 effectTag: 1, firstEffect: null, lastEffect: null, // 單鏈表結構,方便遍歷fiber樹上有副作用的節點 nextEffect: FiberNode|null, // 第一個子fiber child: FiberNode|null, // 指向父fiber,表示當前節點處理完畢后,應該向誰提交自己的結果effect list return: FiberNode|null // 兄弟fiber slibing: FiberNode|null, // 當前父fiber中的位置 index: 0, // fiber實例對象,指向當前組件實例 stateNode: Card, // setState待更新狀態,回調,DOM更新的隊列 updateQueue: null, // 當前UI的狀態,反映了UI當前在屏幕上的表現狀態 memoizedState: {}, // 前次渲染中用於決定UI的props memoizedProps: {}, // 即將應用於下一次渲染更新的props pendingProps: {}, // 和組件Element中的key,ref一致 key: null, ref: null, // fiber更新時基於當前fiber克隆出的鏡像,更新時記錄兩個fiber diff的變化;更新結束后alternate替換之前的fiber成為新的fiber節點 alternate: {}, // 標記子樹上待更新任務的優先級 (最新版的react做了變更,改由過期時間實現,時間越大,setState越頻繁,優先級就越高) pendingWorkPriority: number } |
從上述fiber數據結構,可以看出fiber tree 是一個鏈表結構,通過child, slibing, return完成結構關聯。
current tree && workInProgress tree
在第一次渲染(didMount)之后,React將得到一個 Fiber 樹,它反映了當前UI的工作狀態。這棵樹通常被稱為 current 樹(當前樹)。當 React 開始處理更新時,它會構建一個所謂的 workInProgress 樹(工作過程樹),它反映了要刷新到屏幕的未來狀態。
所有的工作都在 workInProgress 樹的 Fiber 節點上執行。當 React 遍歷 current 樹時,對於每個現有 Fiber 節點,React 會創建一個構成 workInProgress 樹的備用節點,該節點會使用 render 方法返回的 ReactElement 中的數據來創建。處理完更新並完成所有相關工作后,React 將工作完成的備用樹workInProgress刷新到屏幕上。一旦這個 workInProgress 樹在屏幕上輸出,它就會變成 current 樹。
workInProgress 樹可以理解為一個工作快照,或者“工作草稿”,一般用戶不可見。對React來說就是不會顯示更新渲染的中間過程,React先處理所有組件,然后將其一次性更新到屏幕上。
副作用 && 副作用列表
更新完成后可能要調用聲明周期方法,更新ref,或者執行其他方法等等,這些都稱之為“副作用”。副作用定義了在組件更新后需要為組件實例完成的 相關工作。不同類型的組件其副作用各不相同。比如一個DOM元素的副作用和類組件的副作用就不一樣。
副作用列表:收集具有副作用的Fiber節點,從而后續能夠快速遍歷線性列表,執行副作用。firstEffect指針指向列表的開始位置,不同節點間通過nextEffect串聯順序。一般React會按從子節點到父節點的順序逐個執行副作用。
Fiber Reconciler
reconciler過程分為兩個階段:
Reconciliation(也叫render)
目的:確定需要在UI中更新的內容
代碼實質:得到標記了副作用的Fiber節點樹。副作用描述了在下一個commit階段需要完成的工作。
這一過程可中斷。事實上React通過時間分片的方式來處理一個或多個Fiber節點,從而賦予對正在做的工作以暫停,恢復,撤銷重做的能力。這一階段的工作對用戶始終不可見。
將每個fiber節點作為最小工作單位,通過自頂向下逐個遍歷fiber node,構造workInProgress tree(一顆新的fiber tree), 得到patch結果。
這一過程總是從頂層的HostRoot節點開始遍歷,但React會跳過那些已經處理過的Fiber節點,直到找到未完成工作或者需要處理的節點。源碼中的入口函數是renderRoot。
具體過程如下:
1 2 3 4 5 6 7 |
1. 如果當前節點不需要更新,直接clone 子節點, 跳到步驟5;如果需要更新,則修改tag,記錄更新類型 2. 更新當前節點狀態,context,props,state等 3. 調用shouldComponentUpdate, 如果返回值為false,則跳轉步驟5 4. 調用組件實例的render方法,得到一個新的子節點,同時為子節點創建Fiber Node(會盡量復用現有fiber,子節點的增刪也發生在這里) 5. 如果沒有產生child fiber, 該工作單元結束,把effect list歸並到return上,並把當前FiberNode的sibling作為下一個工作單元。如果有child fiber,將child指向作為下一個工作單元。 6. 檢查有沒有剩余時間,如果有繼續執行下一個工作單元;如果沒有,等到下一次主線程空閑時再開始執行下一個工作單元 7. 如果沒有下一個工作單元,回到workInProgress tree 根節點,reconciliation節點結束,進入pendingCommit狀態。 |
從上述過程可以看出,1-6的實際執行邏輯其實是一個work loop,每執行完一次loop,都要檢查有沒有剩余時間,進行控制權的交換。
由於每做完一個loop,都要把effect list向上歸並到return,因此等到loop結束時,workInProgress根節點上的effect list就是收集到的所有effect。
構建workInProgress tree的過程就是diff的過程。
通過requestIdleCallback來調度執行一個任務,每完成一個任務,都回來檢查下有沒有優先級更高的任務。每完成一個任務,都要把時間控制權交換給主線程,直到下一個requestIdleCallback回調再繼續構建workInProgress tree.(requestIdleCallback本身有兼容性問題,react團隊通過MessageChannel + requestAnimationFrame實現了requestIdleCallback的效果)。
PS.Fiber之前的Reconciler被叫做Stack Reconciler,就是因為這些調度上下文信息是由系統堆棧來保存的,以便和Fiber Reconciler區分開。
這一階段執行的生命周期方法有:
componentWillMount、
componentWillReceiveProps、
shouldComponentUpdate、
componentWillUpdate
由於Reconciliation階段是可中斷的,因此處在這一階段的生命周期鈎子函數可能被多次調用,存在副作用,從而引起bug。所以版本16之后會逐漸廢除掉這些API(不包括scu函數)
但 componentWillReceiveProps 和 componentWillUpdate 在實際業務場景中比較有用,所以16新增了兩個API static getDerivedStateFromProps 和 getSnapshotBeforeUpdate 用以解決相同場景下的業務問題。
Commit
目的:更新UI,對DOM應用上一個過程得到的patch結果。
代碼實質:已經得到了標記了副作用的的Fiber節點樹,通過遍歷副作用列表,根據副作用類型執行具體的副作用,包括DOM更新,生命周期函數調用,ref更新等一系列用戶可見的UI變化。
副作用類型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { NoEffect, PerformedWork, Placement, // 掛載,didMount Update, // 更新, didUpdate Snapshot, // getSnapshotBeforeUpdate,更新之前設置快照 PlacementAndUpdate, Deletion, // 卸載,willUnmount ContentReset, Callback, DidCapture, Ref, Incomplete, HostEffectMask, Passive, } from 'shared/ReactSideEffectTags'; |
進入commit階段時,react從上一階段得到了兩棵樹和一個副作用列表。current樹反應當前屏幕上UI的狀態,finishedWork反映未來需要映射到屏幕上UI的狀態。副作用列表來描述需要實際做的操作,比如dom更新,增刪,調用生命周期函數等等。因此嚴格來說,副作用列表應該是finishedWork樹的子集。
這一階段的工作會導致用戶可見的變化,比如DOM更新。因此該過程不可中斷,必須一直執行直到更新完成。
根據副作用類型,執行工作:
1 2 3 4 5 |
1.副作用類型為Snapshot, 則執行getSnapshotBeforeUpdate生命周期;Deletion類型,執行componentWillUnmount生命周期 2.執行DOM更新 3.將 finishedWork 樹設置為 current 4.副作用為Placement類型執行componentDidMount生命周期;Update類型執行componentDidUpdate生命周期 5.其他鈎子 |
這一階段執行的生命周期方法有:
getSnapshotBeforeUpdate、
componentDidMount、
componentDidUpdate、
componentWillUnmount
思考:為什么commit(patch)不能拆分?這樣意義不大,容易導致react內部維護的dom狀態和實際不一致,影響體驗
參考文檔
0.fiber介紹
1.fiber架構
3.fiber 如果404,請先訪問http://makersden.io
4.Andrew Clark筆記簡要