React 调和(Reconciler)原理理解
文章目錄
- Fiber
- Fiber 更新機制
- 雙緩沖樹
- render階段
- commit 階段
- 總結
Fiber
Reactv15及之前,React 對于虛擬 DOM 是采用 遞歸方式 遍歷更新的,一次更新,從應用根部遞歸更新,遞歸開始后中途無法終端,隨著項目復雜,層級變深,導致更新時間變成,給前端交互上的體驗就卡頓。
Fiber 誕生在 Reactv16 版本,Fiber 架構目的就是解決大型 React 應用卡頓,fiber 在 React 中是最小粒度的執行單元,在遍歷更新每一個節點的時候都不是用的真實 DOM ,都是采用虛擬 DOM ,所以可以 理解成 fiber 就是 React 的虛擬 DOM 。
Fiber解決:
- 更新 fiber 的過程叫做 Reconciler(調和器),每一個 fiber 可以作為一個 執行單元 來處理,所以每一個 fiber 可以根據自身的過期時間 expirationTime( v17 版本叫做優先級 lane )來判斷是否還有空間時間執行更新。
- 如果沒有時間更新,就要把主動權交給瀏覽器去渲染,做一些動畫,重排( reflow ),重繪(repaints) 等。
- 等瀏覽器空余時間,再通過 Scheduler (調度器),再次恢復執行單元。在本質上中斷了渲染,提升用戶體驗。
element、fiber、dom 三者間關系:
- element 是 React 視圖層在代碼層級上的表象,也就是開發者寫的 jsx 語法,寫的元素結構,都會被創建成 element 對象的形式。上面保存了 props , children 等信息。
- dom 是元素在瀏覽器上給用戶直觀的表象。
- fiber 可以理解為 element 和真實 dom 之間的交流樞紐站,每一個類型 element 都會有一個與之對應的 fiber 類型,element 變化引起更新流程都是通過 fiber 層面做一次調和改變,然后對于元素,形成新的 dom 做視圖渲染。
element 與 fiber 之間的對應關系:
| FunctionComponent = 0 | 函數組件 |
| ClassComponent = 1 | 類組件 |
| IndeterminateComponent = 2 | 初始化的時候不知道是函數組件還是類組件 |
| HostRoot = 3 | Root Fiber 可以理解為根元素 , 通過reactDom.render()產生的根元素 |
| HostPortal = 4 | ReactDOM.createPortal 產生的 Portal |
| HostComponent = 5 | dom 元素 比如 |
| HostText = 6 | 文本節點 |
| Fragment = 7 | <React.Fragment> |
| Mode = 8 | <React.StrictMode> |
| ContextConsumer = 9 | <Context.Consumer> |
| ContextProvider = 10 | <Context.Provider> |
| `ForwardRef = 11 | React.ForwardRef |
| Profiler = 12 | <Profiler> |
| SuspenseComponent = 13 | <Suspense> |
| MemoComponent = 14 | React.memo 返回的組件 |
fiber 保存信息:
function FiberNode(){this.tag = tag; // fiber 標簽 證明是什么類型fiber。this.key = key; // key調和子節點時候用到。 this.type = null; // dom元素是對應的元素類型,比如div,組件指向組件對應的類或者函數。 this.stateNode = null; // 指向對應的真實dom元素,類組件指向組件實例,可以被ref獲取。this.return = null; // 指向父級fiberthis.child = null; // 指向子級fiberthis.sibling = null; // 指向兄弟fiber this.index = 0; // 索引this.ref = null; // ref指向,ref函數,或者ref對象。this.pendingProps = pendingProps;// 在一次更新中,代表element創建this.memoizedProps = null; // 記錄上一次更新完畢后的propsthis.updateQueue = null; // 類組件存放setState更新隊列,函數組件存放this.memoizedState = null; // 類組件保存state信息,函數組件保存hooks信息,dom元素為nullthis.dependencies = null; // context或是時間的依賴項this.mode = mode; //描述fiber樹的模式,比如 ConcurrentMode 模式this.effectTag = NoEffect; // effect標簽,用于收集effectListthis.nextEffect = null; // 指向下一個effectthis.firstEffect = null; // 第一個effectthis.lastEffect = null; // 最后一個effectthis.expirationTime = NoWork; // 通過不同過期時間,判斷任務是否過期, 在v17版本用lane表示。this.alternate = null; //雙緩存樹,指向緩存的fiber。更新階段,兩顆樹互相交替。 }fiber建立起關聯:
每一個 element 都會對應一個 fiber ,每一個 fiber 是通過 return , child ,sibling 三個屬性建立起聯系的。
- return: 指向父級 Fiber 節點。
- child: 指向子 Fiber 節點。
- sibling:指向兄弟 fiber 節點。
Fiber 更新機制
1、初始化
第一步:創建 fiberRoot 和 rootFiber
- fiberRoot:首次構建應用, 創建一個 fiberRoot ,作為整個 React 應用的根基。
- rootFiber: 通過 ReactDOM.render 渲染出來的。
注意:一個 React 應用可以有多 ReactDOM.render 創建的 rootFiber ,但是只能有一個 fiberRoot(應用根節點)
第二步:workInProgress和current
開始到正式渲染階段,會進入 beginwork 流程
- workInProgress:正在內存中構建的 Fiber 樹稱為 workInProgress Fiber 樹。在一次更新中,所有的更新都是發生在 workInProgress 樹上。在一次更新之后,workInProgress 樹上的狀態是最新的狀態,那么它將變成 current 樹用于渲染視圖。
- current:正在視圖層渲染的樹叫做 current Fiber樹。
rootFiber 的渲染流程: 首先會復用當前 current 樹( rootFiber )的 alternate 作為 workInProgress 。如果沒有 alternate (初始化的 rootFiber 是沒有 alternate ),那么會創建一個 fiber 作為 workInProgress 。會用 alternate 將新創建的 workInProgress 與 current 樹建立起關聯。
currentFiber.alternate = workInProgressFiberworkInProgressFiber.alternate = currentFiber
第三步:深度調和子節點,渲染視圖
在新創建的 alternates 上,完成整個 fiber 樹的遍歷,包括 fiber 的創建。
最后會以 workInProgress 作為最新的渲染樹,fiberRoot 的 current 指針指向 workInProgress 使其變為 current Fiber 樹。到此完成初始化流程。
2、更新
首先會重新創建 workInProgresss 樹,復用當前 current 樹上的 alternate ,作為新的 workInProgress ,
由于初始化 rootfiber 有 alternate ,所以對于剩余的子節點,React 還需要創建一份,和 current 樹上的 fiber 建立起 alternate 關聯。
渲染完畢后,workInProgresss 再次變成 current 樹。
總結:
① 每一個 fiber 可以看作一個執行的單元。
② 在調和過程中,每一個發生更新的 fiber 都會作為一次 workInProgress 。
雙緩沖樹
問題:canvas 繪制動畫的時候,如果上一幀計算量比較大,導致清除上一幀畫面到繪制當前幀畫面之間有較長間隙,就會出現白屏。
解決: canvas 在內存中繪制當前動畫,繪制完畢后直接用當前幀替換上一幀畫面,由于省去了兩幀替換間的計算時間,不會出現從白屏到出現畫面的閃爍情況。
這種在內存中構建并直接替換的技術叫做 雙緩存。
React 用 workInProgress 樹(內存中構建的樹) 和 current (渲染樹) 來實現更新邏輯。雙緩存一個 在內存中構建,一個 渲染視圖,兩顆樹用 alternate 指針相互指向,在下一次渲染的時候,直接復用緩存樹做為下一次渲染樹,上一次的渲染樹又作為緩存樹,這樣可以 防止只用一棵樹更新狀態的丟失的情況,又加快了 DOM 節點的替換與更新。
render階段
1、workLoop
fiber 的遍歷開始—— workLoop,workLoop 就是執行每一個單元的 調度器,如果渲染沒有被中斷,那么 workLoop 會遍歷一遍 fiber 樹。
2、performUnitOfWork
performUnitOfWork 包括兩個階段 beginWork 和 completeWork。
- beginWork:是 向下調和 的過程。就是由 fiberRoot 按照 child 指針逐層向下調和,期間會執行函數組件,實例類組件,diff 調和子節點,打不同 effectTag。
- completeUnitOfWork:是 向上歸并 的過程。如果有兄弟節點,會返回 sibling兄弟,沒有返回 return 父級,一直返回到 fiebrRoot ,期間可以形成 effectList,對于初始化流程會創建 DOM ,對于 DOM 元素進行事件收集,處理style,className等。
3、beiginWork
beiginWork 作用:
- 對于組件,執行部分生命周期,執行 render ,得到最新的 children 。
- 向下遍歷調和 children ,復用 oldFiber ( diff 算法)
- 打不同的副作用標簽 effectTag ,比如類組件的生命周期,或者元素的增加,刪除,更新。
常用的 effectTag:
| Placement =0b0000000000010 | 插入節點 |
| Update = 0b0000000000100 | 更新fiber |
| Deletion = 0b0000000001000 | 刪除fiebr |
| Snapshot = 0b0000100000000 | 快照 |
| Passive =0b0001000000000 | useEffect的副作用 |
| Callback =0b0000000100000 | setState的 callback |
| Ref = 0b0000010000000 | ref |
4、completeUnitOfWork
- completeUnitOfWork 會將 effectTag 的 Fiber 節點會被保存在一條被稱為 effectList 的單向鏈表中。在 commit 階段,將不再需要遍歷每一個 fiber ,只需要執行更新 effectList 。
- 對于組件處理 context ;對于元素標簽初始化,會創建真實 DOM ,將子孫 DOM 節點插入剛生成的 DOM 節點中;會觸發 diffProperties 處理 props ,比如事件收集,style,className 處理。
commit 階段
- 一方面是 對一些生命周期和副作用鉤子的處理,比如 componentDidMount ,函數組件的 useEffect ,useLayoutEffect ;
- 另一方面就是 在一次更新中,添加節點( Placement ),更新節點( Update ),刪除節點( Deletion ),還有就是 一些細節的處理,比如 ref 的處理。
commit 可以分為:
- Before mutation 階段(執行 DOM 操作前);
- mutation 階段(執行 DOM 操作);
- layout 階段(執行 DOM 操作后)
1、Before mutation
- 還沒修改真實的 DOM ,是 獲取 DOM 快照的最佳時期,如果是類組件有 getSnapshotBeforeUpdate ,那么會執行這個生命周期。
- 會 異步調用 useEffect , useEffect 是采用異步調用的模式,其目的就是 防止同步執行時阻塞瀏覽器做視圖渲染。
2、Mutation
- 置空 ref ,對于 ref 的處理。
- 對新增元素,更新元素,刪除元素。進行真實的 DOM 操作。
3、Layout
- commitLayoutEffectOnFiber 對于類組件,會執行生命周期,setState 的callback,對于函數組件會執行 useLayoutEffect 鉤子。
- 如果有 ref ,會 重新賦值 ref 。
調和+異步調度 原理圖
總結
1、Fiber組成
2、Fiber 更新機制(初始化和更新)、雙緩沖樹
3、Fiber 調和過程(render和commit階段)
總結
以上是生活随笔為你收集整理的React 调和(Reconciler)原理理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue判断什么手机打开网页及是否用QQ浏
- 下一篇: 联想RS550服务器安装Ubuntu16