天天看點

React知識點總結(七)React知識點總結(Fiber&Diff)一、Fiber模型是什麼二、Fiber樹是如何建構/更新的三、Diff算法四、狀态更新五、優先級六、commit階段中的useEffect執行時機

React知識點總結(Fiber&Diff)

文章目錄

  • React知識點總結(Fiber&Diff)
  • 一、Fiber模型是什麼
    • 1.代數效應
    • 2.代數效應在React中的展現
    • 3.React Fiber
    • 4.Fiber的起源
    • Fiber的結構
      • (一)作為架構
      • (二)作為靜态的資料結構
      • (三)作為動态的更新單元
  • 二、Fiber樹是如何建構/更新的
    • 1.“遞”與“歸”
      • “遞”
      • “歸”
    • 2.beginWork方法都做了什麼?
    • 3.completeWork做了什麼?
  • 三、Diff算法
    • 單節點
    • 多節點
      • 第一輪
      • 第二輪
        • 1.newChildren與oldFiber同時周遊完
        • 2.newChildren沒周遊完,oldFiber周遊完
        • 3.newChildren周遊完,oldFiber沒周遊完
        • 4.newChildren與oldFiber都沒周遊完
  • 四、狀态更新
    • 狀态更新的流程圖
    • Update
      • Update的分類
      • Update與fiber的關系(class元件)
  • 五、優先級
    • 如何保證Update對象不丢失
    • 如何保證狀态依賴的連續性
  • 六、commit階段中的useEffect執行時機

一、Fiber模型是什麼

1.代數效應

代數效應是函數式程式設計中的一個概念,是指将函數的副作用抽離出函數,隻關心函數的功能。(可以簡單了解為在不同使用場景像替代數字一樣簡單。。。在使用時使用規則不會變化,來限制使用)

2.代數效應在React中的展現

典型的例子就是

React

中的

hooks

,我們不需要關心内部實作,且在函數元件中使用時用法都相同,不會有什麼副作用,隻需關注業務邏輯即可。

function App() {
  const [num, updateNum] = useState(0);
  
  return (
    <button onClick={() => updateNum(num => num + 1)}>{num}</button>  
  )
}
           

3.React Fiber

React Fiber

React

一套狀态更新機制,支援中斷和恢複以及不同優先級,更新單元為

React Element

對應的

Fiber

對象。

4.Fiber的起源

在React15及以前,

Reconciler

采用遞歸的方式建立虛拟DOM,遞歸過程是不能中斷的。如果元件樹的層級很深,遞歸會占用線程很多時間,造成卡頓。

為了解決這個問題,React16将

遞歸的無法中斷的更新

重構為

異步的可中斷更新

,由于曾經用于遞歸的虛拟DOM資料結構已經無法滿足需要。于是,全新的Fiber架構應運而生。

Fiber的結構

(一)作為架構

每個

Fiber

對象是靠什麼來構成

Fiber

樹的呢,靠一下三個屬性:

// 指向父級Fiber節點
this.return = null;
// 指向子Fiber節點
this.child = null;
// 指向右邊第一個兄弟Fiber節點
this.sibling = null;
           

如:

React知識點總結(七)React知識點總結(Fiber&amp;Diff)一、Fiber模型是什麼二、Fiber樹是如何建構/更新的三、Diff算法四、狀态更新五、優先級六、commit階段中的useEffect執行時機

(二)作為靜态的資料結構

每個Fiber對象儲存了

DOM

對象/元件相關的資訊

// Fiber對應元件的類型 Function/Class/Host...
this.tag = tag;
// key屬性
this.key = key;
// 大部分情況同type,某些情況不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 對于 FunctionComponent,指函數本身,對于ClassComponent,指class,對于HostComponent,指DOM節點tagName
this.type = null;
// Fiber對應的真實DOM節點
this.stateNode = null;
           

(三)作為動态的更新單元

每個

Fiber

對象儲存了此次更新相關的資訊:

// 儲存本次更新造成的狀态改變相關資訊
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 儲存本次更新會造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

// 排程優先級相關
this.lanes = NoLanes;
this.childLanes = NoLanes;
           

二、Fiber樹是如何建構/更新的

這一過程發生在

render

階段

1.“遞”與“歸”

通過周遊的方式實作可以中斷的遞歸:

“遞”

rootFiber

開始深度優先周遊,每一個

Fiber

對象都調用

beginWork

方法,這個方法會為傳入的

Fiber

對象建立子

Fiber

對象,并将兩者連接配接起來。當周遊到葉子節點(沒有子元件了)就進入“歸”階段

“歸”

在此階段會調用

completeWork

方法,來處理

Fiber

對象。若目前對象還有兄弟節點,則處理完目前節點後會進入兄弟節點的“遞”階段。當執行到

rootFiber

render

階段就結束了。

2.beginWork方法都做了什麼?

React知識點總結(七)React知識點總結(Fiber&amp;Diff)一、Fiber模型是什麼二、Fiber樹是如何建構/更新的三、Diff算法四、狀态更新五、優先級六、commit階段中的useEffect執行時機

3.completeWork做了什麼?

React知識點總結(七)React知識點總結(Fiber&amp;Diff)一、Fiber模型是什麼二、Fiber樹是如何建構/更新的三、Diff算法四、狀态更新五、優先級六、commit階段中的useEffect執行時機

三、Diff算法

一般的

Diff

算法,時間複雜度在

O(n3)

,

React

為了提升效率,提出了三個限制條件:

  1. 按層來比較元素,如果跨層則不會進行移動操作,而會重新生成元素。
  2. 不同的類型的節點會産生不同的樹如果元素由

    div

    變為

    p

    React

    會銷毀

    div

    及其子孫節點,并建立

    p

    及其子孫節點。
  3. 使用

    key

    屬性來定位元素

    Diff

    前後位置的變化

可以将

Diff

算法分為兩類:單節點和多節點(指的是

workInProgressFiber

,即新的

fiber

節點)。

單節點

React通過先判斷

key

是否相同,如果

key

相同則判斷

type

是否相同,隻有都相同時一個

DOM

節點才能複用。

注意

1.當

child

!==

null

key

相同且

type

不同時執行

deleteRemainingChildren

child

及其兄弟

fiber

都标記删除。

2.當

child

!==

null

key

不同時僅将

child

标記删除。

如:

// 目前頁面顯示的
ul > li * 3

// 這次需要更新的
ul > p
           

p

key

li

key

不同,說明目前以及後面的兄弟元素都不可能有相同的

key

,則沒有必要繼續周遊,不可複用之前的節點,需生成新的節點。

多節點

分兩輪周遊

第一輪

  1. let i = 0

    ,周遊

    newChildren

    ,将

    newChildren[i]

    oldFiber

    比較,判斷

    DOM

    節點是否可複用。
  2. 如果可複用,

    i++

    ,繼續比較

    newChildren[i]

    oldFiber.sibling

    ,可以複用則繼續周遊。
  3. 如果不可複用,分兩種情況:

    key

    不同導緻不可複用,立即跳出整個周遊,第一輪周遊結束。

    key

    相同

    type

    不同導緻不可複用,會将

    oldFiber

    标記為

    DELETION

    ,并繼續周遊。
  4. 如果

    newChildren

    周遊完(即i === newChildren.length -

    1)或者oldFiber周遊完(即oldFiber.sibling === null),跳出周遊,第一輪周遊結束。

第二輪

1.newChildren與oldFiber同時周遊完

那就是最理想的情況:隻需在第一輪周遊進行元件更新 。此時

Diff

結束。

2.newChildren沒周遊完,oldFiber周遊完

已有的

DOM

節點都複用了,這時還有新加入的節點,意味着本次更新有新節點插入,我們隻需要周遊剩下的

newChildren

為生成的

workInProgress fiber

依次标記

Placement

3.newChildren周遊完,oldFiber沒周遊完

意味着本次更新比之前的節點數量少,有節點被删除了。是以需要周遊剩下的

oldFiber

,依次标記

Deletion

4.newChildren與oldFiber都沒周遊完

這意味着有節點在這次更新中改變了位置。

用一個簡單的Demo來看一下整個判斷的過程

// 之前
abcd

// 之後
acdb

===第一輪周遊開始===
a(之後)vs a(之前)  
key不變,可複用
此時 a 對應的oldFiber(之前的a)在之前的數組(abcd)中索引為0
是以 lastPlacedIndex = 0;

繼續第一輪周遊...

c(之後)vs b(之前)  
key改變,不能複用,跳出第一輪周遊
此時 lastPlacedIndex === 0;
===第一輪周遊結束===

===第二輪周遊開始===
newChildren === cdb,沒用完,不需要執行删除舊節點
oldFiber === bcd,沒用完,不需要執行插入新節點

将剩餘oldFiber(bcd)儲存為map

// 目前oldFiber:bcd
// 目前newChildren:cdb

繼續周遊剩餘newChildren

key === c 在 oldFiber中存在
const oldIndex = c(之前).index;
此時 oldIndex === 2;  // 之前節點為 abcd,是以c.index === 2
比較 oldIndex 與 lastPlacedIndex;

如果 oldIndex >= lastPlacedIndex 代表該可複用節點不需要移動
并将 lastPlacedIndex = oldIndex;
如果 oldIndex < lastplacedIndex 該可複用節點之前插入的位置索引小于這次更新需要插入的位置索引,代表該節點需要向右移動

在例子中,oldIndex 2 > lastPlacedIndex 0,
則 lastPlacedIndex = 2;
c節點位置不變

繼續周遊剩餘newChildren

// 目前oldFiber:bd
// 目前newChildren:db

key === d 在 oldFiber中存在
const oldIndex = d(之前).index;
oldIndex 3 > lastPlacedIndex 2 // 之前節點為 abcd,是以d.index === 3
則 lastPlacedIndex = 3;
d節點位置不變

繼續周遊剩餘newChildren

// 目前oldFiber:b
// 目前newChildren:b

key === b 在 oldFiber中存在
const oldIndex = b(之前).index;
oldIndex 1 < lastPlacedIndex 3 // 之前節點為 abcd,是以b.index === 1
則 b節點需要向右移動
===第二輪周遊結束===

最終acd 3個節點都沒有移動,b節點被标記為移動
           

四、狀态更新

狀态更新的流程圖

觸發狀态更新(根據場景調用不同方法)

    |
    |
    v

建立Update對象(給觸發狀态更新的fiber對象建立)

    |
    |
    v

從fiber到root(`markUpdateLaneFromFiberToRoot`由于render階段是從rootFiber向下執行的,為了得到rootFiber,從觸發狀态更新的fiber得到rootFiber,同時包含給不同的fiber标記優先級)

    |
    |
    v

排程更新(`ensureRootIsScheduled`)

    |
    |
    v

render階段(`performSyncWorkOnRoot` 或 `performConcurrentWorkOnRoot` 同步更新還是異步更新)

    |
    |
    v

commit階段(`commitRoot`)
           

Update

Update的分類

  • ReactDOM.render —— HostRoot
  • this.setState —— ClassComponent
  • this.forceUpdate —— ClassComponent
  • useState —— FunctionComponent
  • useReducer —— FunctionComponent

Update與fiber的關系(class元件)

fiber

節點存在着

updateQueue

updateQueue

為儲存更新對象的連結清單。

fiber.updateQueue.baseUpdate: u1 --> u2 --> u3 --> u4
           

接下來周遊updateQueue.baseUpdate連結清單,以

fiber.updateQueue.baseState

為初始

state

,依次與周遊到的每個

Update

計算并産生新的

state

。在周遊時如果有優先級低的

Update

會被跳過。

state

的變化在

render

階段産生與上次更新不同的

JSX

對象,通過

Diff

算法産生

effectTag

,在

commit

階段渲染在頁面上。

五、優先級

通過一張圖來了解一下

React

的優先級:

React知識點總結(七)React知識點總結(Fiber&amp;Diff)一、Fiber模型是什麼二、Fiber樹是如何建構/更新的三、Diff算法四、狀态更新五、優先級六、commit階段中的useEffect執行時機

在這個例子中,

u1

的優先級較

u2

的優先級低,首先

u1

進入

render

階段,當産生

u2

時,中斷

u1

u2

進入

render

階段。此時,該

Update

對象儲存的連結清單是:

u1 -- u2
           

由于

u1

的優先級較低,

u1

會被跳過,

u2

會進入

render-commit

階段。在

commit

階段再排程一次更新,這次是基于

baseState

firstBaseUpdate

儲存的

u1

,開啟一次新的

render

階段。

如何保證Update對象不丢失

render

階段,

shared.pending

的環被剪開并連接配接在

updateQueue.lastBaseUpdate

後面。實際上

shared.pending

會被同時連接配接在

workInProgress updateQueue.lastBaseUpdate

current updateQueue.lastBaseUpdate

後面。

當開啟一個新的

render

時,會基于

current updateQueue

克隆出

workInProgress updateQueue

,是以不會丢失。

如何保證狀态依賴的連續性

當某個

Update

優先級低于目前優先級時,目前

Update

對象和之後的

Update

對象會被跳過,儲存到

baseUpdate

中作為下次

Update

更新的

Update

。且目前的

baseState

為更新後的

state

,不是被跳過時的

state

六、commit階段中的useEffect執行時機

  1. before mutation

    階段在

    scheduleCallback

    中排程

    flushPassiveEffects

  2. layout

    階段之後将

    effectList

    指派給

    rootWithPendingPassiveEffects

  3. scheduleCallback

    觸發

    flushPassiveEffects

    flushPassiveEffects

    内部周遊

    rootWithPendingPassiveEffects

參考文章:

React技術揭秘