「譯」React Fiber 那些事: 深入解析新的協調演算法

ES2049發表於2018-12-03

翻譯自:Inside Fiber: in-depth overview of the new reconciliation algorithm in React

React 是一個用於構建使用者互動介面的 JavaScript 庫,其核心 機制 就是跟蹤元件的狀態變化,並將更新的狀態對映到到新的介面。在 React 中,我們將此過程稱之為協調。我們呼叫 setState 方法來改變狀態,而框架本身會去檢查 state 或 props 是否已經更改來決定是否重新渲染元件。

React 的官方文件對 協調機制 進行了良好的抽象描述: React 的元素、生命週期、 render 方法,以及應用於元件子元素的 diffing 演算法綜合起到的作用,就是協調。從 render 方法返回的不可變的 React 元素通常稱為「虛擬 DOM」。這個術語有助於早期向人們解釋 React,但它也引起了混亂,並且不再用於 React 文件。在本文中,我將堅持稱它為 React 元素的樹。

除了 React 元素的樹之外,框架總是在內部維護一個例項來持有狀態(如元件、 DOM 節點等)。從版本 16 開始, React 推出了內部例項樹的新的實現方法,以及被稱之為 Fiber 的演算法。如果想要了解 Fiber 架構帶來的優勢,可以看下 React 在 Fiber 中使用連結串列的方式和原因

這是本系列的第一篇文章,這一系列的目的就是向你描繪出 React 的內部架構。在本文中,我希望能夠提供一些與演算法相關的重要概念和資料結構,並對其進行深入闡述。一旦我們有足夠的背景,我們將探索用於遍歷和處理 Fiber 樹的演算法和主要功能。本系列的下一篇文章將演示 React 如何使用該演算法執行初始渲染和處理 state 以及 props 的更新。到那時,我們將繼續討論排程程式的詳細資訊,子協調過程以及構建 effect 列表的機制。

我將給你帶來一些非常高階的知識?。我鼓勵你閱讀來了解 Concurrent React 的內部工作的魔力。如果您計劃開始為 React 貢獻程式碼,本系列也將為您提供很好的指導。我是 逆向工程的堅定信徒,因此本文會有很多最新版本 16.6.0 中的原始碼的連結。

需要消化的內容絕對是很多的,所以如果你當下還不能很理解的話,不用感到壓力。花些時間是值得的。請注意,只是使用 React 的話,您不需要知道任何文中的內容。本文是關於 React 在內部是如何工作的。

我在 ag-Grid 擔任開發人員倡導者。如果您想了解資料網格或尋找最終的 React 資料網格解決方案,請與我們聯絡或嘗試使用指南「在5分鐘內開始使用 React 網格」。我很樂意回答您可能會有的任何問題。

背景介紹

如下是我將在整個系列中使用的一個簡單的應用程式。我們有一個按鈕,點選它將會使螢幕上渲染的數字加 1:

「譯」React Fiber 那些事: 深入解析新的協調演算法

而它的實現如下:

class ClickCounter extends React.Component { 
constructor(props) {
super(props);
this.state = {count: 0
};
this.handleClick = this.handleClick.bind(this);

} handleClick() {
this.setState((state) =>
{
return {count: state.count + 1
};

});

} render() {
return [ <
button key="1" onClick={this.handleClick
}>
Update counter<
/button>
, <
span key="2">
{this.state.count
}<
/span>
]
}
}複製程式碼

你可以在 這裡 把玩一下。如您所見,它就是一個可從 render 方法返回兩個子元素 — buttonspan 的簡單元件。只要你單擊該按鈕,元件的狀態將在處理程式內被更新,而狀態的更新就會導致 span 元素內的文字更新。

在協調階段內,React 進行了各種各樣的活動。例如,在我們的簡單應用程式中,從第一次渲染到狀態更新後的期間內,React 執行了如下高階操作:

  • 更新了 ClickCounter 元件的內部狀態的 count 屬性

  • 獲取和比較了 ClickCounter 元件的子元件及其 props

  • 更新 span 元素的 props

  • 更新 span 元素的 textContent 屬性

除了上述活動,React 在協調期間還執行了一些其他活動,如呼叫 生命週期方法 或更新 refs。**所有這些活動在 Fiber 架構中統稱為「工作」。**工作型別通常取決於 React 元素的型別。例如,對於類定義的元件,React 需要建立例項,但是函式定義的元件就不必執行此操作。正如我們所瞭解的,React 中有許多元素型別,例如:類和函式元件,宿主元件(DOM 節點)portal 等。React 元素的型別由 createElement 函式的第一個引數定義,此函式通常在 render 方法中呼叫以建立元素。

在我們開始探索活動細節和 Fiber 演算法的主要內容之前,我們首先來熟悉下 React 在內部使用的一些資料結構。

從 React 元素到 Fiber 節點

React 中的每個元件都有一個 UI 表示,我們可以稱之為從 render 方法返回的一個檢視或模板。這是 ClickCounter 元件的模板:

<
button key="1" onClick={this.onClick
}>
Update counter<
/button>
<
span key="2">
{this.state.count
}<
/span>
複製程式碼

React 元素

如果模板經過 JSX 編譯器處理,你就會得到一堆 React 元素。這是從 React 元件的 render 方法返回的,但並不是 HTML 。由於我們並沒有被強制要求使用 JSX,因此我們的 ClickCounter 元件的 render 方法可以像這樣重寫:

class ClickCounter { 
... render() {
return [ React.createElement( 'button', {
key: '1', onClick: this.onClick
}, 'Update counter' ), React.createElement( 'span', {
key: '2'
}, this.state.count ) ]
}
}複製程式碼

render 方法中呼叫的 React.createElement 會產生兩個如下的資料結構:

[    { 
$$typeof: Symbol(react.element), type: 'button', key: "1", props: {
children: 'Update counter', onClick: () =>
{
...
}
}
}, {
$$typeof: Symbol(react.element), type: 'span', key: "2", props: {
children: 0
}
}]複製程式碼

可以看到,React 為這些物件新增了 $$typeof 屬性,從而將它們唯一地標識為 React 元素。此外我們還有屬性 typekeyprops 來描述元素。這些值取自你傳遞給 React.createElement 函式的引數。請注意React 如何將文字內容表示為 spanbutton 節點的子項,以及 click 鉤子如何成為 button 元素 props 的一部分。 React 元素上還有其他欄位,如 ref 欄位,而這超出了本文的範圍。

ClickCounter 的 React 元素就沒有什麼 props 或 key 屬性:

{ 
$$typeof: Symbol(react.element), key: null, props: {
}, ref: null, type: ClickCounter
}複製程式碼

Fiber 節點

協調期間,從 render 方法返回的每個 React 元素的資料都會被合併到 Fiber 節點樹中。每個 React 元素都有一個相應的 Fiber 節點。與 React 元素不同,不會在每次渲染時重新建立這些 Fiber 。這些是持有元件狀態和 DOM 的可變資料結構。

我們之前討論過,根據不同 React 元素的型別,框架需要執行不同的活動。在我們的示例應用程式中,對於類元件 ClickCounter ,它呼叫生命週期方法和 render 方法,而對於 span 宿主元件(DOM 節點),它進行得是 DOM 修改。因此,每個 React 元素都會轉換為 相應型別 的 Fiber 節點,用於描述需要完成的工作。

您可以將 Fiber 視為表示某些要做的工作的資料結構,或者說,是一個工作單位。Fiber 的架構還提供了一種跟蹤、規劃、暫停和銷燬工作的便捷方式。

當 React 元素第一次轉換為 Fiber 節點時,React 在 createFiberFromTypeAndProps 函式中使用元素中的資料來建立 Fiber。在隨後的更新中,React 會再次利用 Fiber 節點,並使用來自相應 React 元素的資料更新必要的屬性。如果不再從 render 方法返回相應的 React 元素,React 可能還需要根據 key 屬性來移動或刪除層級結構中的節點。

檢視 ChildReconciler 函式以檢視 React 為現有 Fiber 節點執行的所有活動和相應函式的列表。

因為React為每個 React 元素建立一個 Fiber 節點,並且因為我們有一個這些元素組成的樹,所以我們可以得到一個 Fiber 節點樹。對於我們的示例應用程式,它看起來像這樣:

「譯」React Fiber 那些事: 深入解析新的協調演算法

所有 Fiber 節點都通過連結串列連線,具體是使用Fiber節點上的 childsiblingreturn 屬性。至於它為什麼以這種方式工作,如果您還沒有閱讀過我的文章,更多詳細資訊請檢視 React 在 Fiber 中使用連結串列的方法和原因

current 樹及 workInProgress 樹

在第一次渲染之後,React 最終得到一個 Fiber 樹,它反映了用於渲染 UI 的應用程式的狀態。這棵樹通常被稱為 current 樹(當前樹)。當 React 開始處理更新時,它會構建一個所謂的 workInProgress 樹(工作過程樹),它反映了要重新整理到螢幕的未來狀態。

所有工作都在 workInProgress 樹的 Fiber 節點上執行。當 React 遍歷 current 樹時,對於每個現有 Fiber 節點,React 會建立一個構成 workInProgress 樹的備用節點,這一節點會使用 render 方法返回的 React 元素中的資料來建立。處理完更新並完成所有相關工作後,React 將準備好一個備用樹以重新整理到螢幕。一旦這個 workInProgress 樹在螢幕上呈現,它就會變成 current 樹。

React 的核心原則之一是一致性。 React 總是一次性更新 DOM – 它不會顯示部分中間結果。workInProgress 樹充當使用者不可見的「草稿」,這樣 React 可以先處理所有元件,然後將其更改重新整理到螢幕。

在原始碼中,您將看到很多函式從 currentworkInProgress 樹中獲取 Fiber 節點。這是一個這類函式的簽名:

function updateHostComponent(current, workInProgress, renderExpirationTime) {...
}複製程式碼

每個Fiber節點持有備用域在另一個樹的對應部分的引用。來自 current 樹中的節點會指向 workInProgress 樹中的節點,反之亦然。

副作用

我們可以將 React 中的一個元件視為一個使用 state 和 props 來計算 UI 表示的函式。其他所有活動,如改變 DOM 或呼叫生命週期方法,都應該被視為副作用,或者簡單地說是一種效果。文件中 是這樣描述的:

您之前可能已經在 React 元件中執行資料提取,訂閱或手動更改 DOM。我們將這些操作稱為“副作用”(或簡稱為“效果”),因為它們會影響其他元件,並且在渲染過程中無法完成。

您可以看到大多 state 和 props 更新都會導致副作用。既然使用副作用是工作(活動)的一種型別,Fiber 節點是一種方便的機制來跟蹤除了更新以外的效果。每個 Fiber 節點都可以具有與之相關的副作用,它們可在 effectTag 欄位中編碼。

因此,Fiber 中的副作用基本上定義了處理更新後需要為例項完成的 工作。對於宿主元件(DOM 元素),所謂的工作包括新增,更新或刪除元素。對於類元件,React可能需要更新 refs 並呼叫 componentDidMountcomponentDidUpdate 生命週期方法。對於其他型別的 Fiber ,還有相對應的其他副作用。

副作用列表

React 處理更新的素對非常迅速,為了達到這種水平的效能,它採用了一些有趣的技術。**其中之一是構建具有副作用的 Fiber 節點的線性列表,從而能夠快速遍歷。**遍歷線性列表比樹快得多,並且沒有必要在沒有副作用的節點上花費時間。

此列表的目標是標記具有 DOM 更新或其他相關副作用的節點。此列表是 finishedWork 樹的子集,並使用 nextEffect 屬性而不是 currentworkInProgress 樹中使用的 child 屬性進行連結。

Dan Abramov 為副作用列表提供了一個類比。他喜歡將它想象成一棵聖誕樹,「聖誕燈」將所有有效節點捆綁在一起。為了使這個視覺化,讓我們想象如下的 Fiber 節點樹,其中標亮的節點有一些要做的工作。例如,我們的更新導致 c2 被插入到 DOM 中,d2c1 被用於更改屬性,而 b2 被用於觸發生命週期方法。副作用列表會將它們連結在一起,以便 React 稍後可以跳過其他節點:

「譯」React Fiber 那些事: 深入解析新的協調演算法

可以看到具有副作用的節點是如何連結在一起的。當遍歷節點時,React 使用 firstEffect 指標來確定列表的開始位置。所以上面的圖表可以表示為這樣的線性列表:

「譯」React Fiber 那些事: 深入解析新的協調演算法

如您所見,React 按照從子到父的順序應用副作用。

Fiber 樹的根節點

每個 React 應用程式都有一個或多個充當容器的 DOM 元素。在我們的例子中,它是帶有 ID 為 containerdiv 元素。React 為每個容器建立一個 Fiber 根 物件。您可以使用對 DOM 元素的引用來訪問它:

const fiberRoot = query('#container')._reactRootContainer._internalRoot複製程式碼

這個 Fiber 根是React儲存對 Fiber 樹的引用的地方,它儲存在 Fiber 根物件的 current 屬性中:

const hostRootFiberNode = fiberRoot.current複製程式碼

Fiber 樹以 一個特殊型別 的 Fiber 節點 HostRoot 開始。它在內部建立的,並充當最頂層元件的父級。HostRoot 節點可通過 stateNode 屬性返回到 FiberRoot

fiberRoot.current.stateNode === fiberRoot;
// true複製程式碼

你可以通過 Fiber 根訪問最頂層的 HostRoot 節點來探索 Fiber 樹,或者可以從元件例項中獲取單獨的 Fiber 節點,如下所示:

compInstance._reactInternalFiber複製程式碼

Fiber 節點結構

現在讓我們看一下為 ClickCounter 元件建立的 Fiber 節點的結構

{ 
stateNode: new ClickCounter, type: ClickCounter, alternate: null, key: null, updateQueue: null, memoizedState: {count: 0
}, pendingProps: {
}, memoizedProps: {
}, tag: 1, effectTag: 0, nextEffect: null
}複製程式碼

以及 span DOM 元素:

{ 
stateNode: new ClickCounter, type: ClickCounter, alternate: null, key: null, updateQueue: null, memoizedState: {count: 0
}, pendingProps: {
}, memoizedProps: {
}, tag: 1, effectTag: 0, nextEffect: null
}複製程式碼

Fiber 節點上有很多欄位。我在前面的部分中描述了欄位 alternateeffectTagnextEffect 的用途。現在讓我們看看為什麼需要其他欄位。

stateNode

儲存元件的類例項、DOM 節點或與 Fiber 節點關聯的其他 React 元素型別的引用。總的來說,我們可以認為該屬性用於保持與一個 Fiber 節點相關聯的區域性狀態。

type

定義此 Fiber 節點的函式或類。對於類元件,它指向建構函式,對於 DOM 元素,它指定 HTML 標記。我經常使用這個欄位來理解 Fiber 節點與哪個元素相關。

tag

定義 Fiber 的型別。它在協調演算法中用於確定需要完成的工作。如前所述,工作取決於React元素的型別。函式 createFiberFromTypeAndProps 將 React 元素對映到相應的 Fiber 節點型別。在我們的應用程式中,ClickCounter 元件的屬性 tag 是 1,表示是 ClassComponent(類元件),而 span 元素的屬性 tag 是 5,表示是 HostComponent(宿主元件)。

updateQueue

狀態更新、回撥和 DOM 更新的佇列。

memoizedState

用於建立輸出的 Fiber 狀態。處理更新時,它會反映當前在螢幕上呈現的狀態。

memoizedProps

在前一個渲染中用於建立輸出的 Fiber 的 props。

pendingProps

已從 React 元素中的新資料更新並且需要應用於子元件或 DOM 元素的 props。

key

唯一識別符號,當具有一組子元素的時候,可幫助 React 確定哪些項發生了更改、新增或刪除。它與 此處 描述的 React 的「列表和鍵」功能有關。

您可以在 此處 找到 Fiber 節點的完整結構。我在上面的解釋中省略了一堆欄位。特別是,我跳過了指標 childsiblingreturn,它們構成了我在 上一篇文章 中描述的樹資料結構。以及專屬於 Scheduler 的 expirationTime,childExpirationTime 和 mode 等欄位類別。

通用演算法

React 在兩個主要階段執行工作:rendercommit

在第一個 render 階段,React 通過 setUpdateReact.render 計劃性的更新元件,並確定需要在 UI 中更新的內容。如果是初始渲染,React 會為 render 方法返回的每個元素建立一個新的 Fiber 節點。在後續更新中,現有 React 元素的 Fiber 節點將被重複使用和更新。這一階段是為了得到標記了副作用的 Fiber 節點樹。副作用描述了在下一個commit階段需要完成的工作。在當前階段,React持有標記了副作用的 Fiber 樹並將其應用於例項。它遍歷副作用列表、執行 DOM 更新和使用者可見的其他更改。

** 我們需要重點理解的是,第一個 render 階段的工作是可以非同步執行的。**React 可以根據可用時間片來處理一個或多個 Fiber 節點,然後停下來暫存已完成的工作,並轉而去處理某些事件,接著它再從它停止的地方繼續執行。但有時候,它可能需要丟棄完成的工作並再次從頂部開始。由於在此階段執行的工作不會導致任何使用者可見的更改(如 DOM 更新),因此暫停行為才有了意義。**與之相反的是,後續 commit 階段始終是同步的。**這是因為在此階段執行的工作會導致使用者可見的變化,例如 DOM 更新。這就是為什麼 React 需要在一次單一過程中完成這些更新。

React 要做的一種工作就是呼叫生命週期方法。一些方法是在 render 階段呼叫的,而另一些方法則是在 commit 階段呼叫。這是在第一個 render 階段呼叫的生命週期列表:

  • [UNSAFE_]componentWillMount(棄用)

  • [UNSAFE_]componentWillReceiveProps(棄用)

  • getDerivedStateFromProps

  • shouldComponentUpdate

  • [UNSAFE_]componentWillUpdate(棄用)

  • render

正如你所看到的,從版本 16.3 開始,在 render 階段執行的一些保留的生命週期方法被標記為 UNSAFE,它們現在在文件中被稱為遺留生命週期。它們將在未來的 16.x 釋出版本中棄用,而沒有 UNSAFE 字首的方法將在 17.0 中移除。您可以在 此處 詳細瞭解這些更改以及建議的遷移路徑。

那麼這麼做的目的是什麼呢?

好吧,我們剛剛瞭解到,因為 render 階段不會產生像 DOM 更新這樣的副作用,所以 React 可以非同步處理元件的非同步更新(甚至可能在多個執行緒中執行)。但是,標有 UNSAFE 的生命週期經常被誤解和濫用。開發人員傾向於將帶有副作用的程式碼放在這些方法中,這可能會導致新的非同步渲染方法出現問題。雖然只有沒有 UNSAFE 字首的對應方法將被刪除,但它們仍可能在即將出現的併發模式(您可以選擇退出)中引起問題。

接下來羅列的生命週期方法是在第二個 commit 階段執行的:

  • getSnapshotBeforeUpdate

  • componentDidMount

  • componentDidUpdate

  • componentWillUnmount

因為這些方法都在同步的 commit 階段執行,他們可能會包含副作用,並對 DOM 進行一些操作。

至此,我們已經有了充分的背景知識,下面我們可以看下用來遍歷樹和執行一些工作的通用演算法。

Render 階段

協調演算法始終使用 renderRoot 函式從最頂層的 HostRoot 節點開始。不過,React 會略過已經處理過的 Fiber 節點,直到找到未完成工作的節點。例如,如果在元件樹中的深層元件中呼叫 setState 方法,則 React 將從頂部開始,但會快速跳過各個父項,直到它到達呼叫了 setState 方法的元件。

工作迴圈的主要步驟

所有的 Fiber 節點都會在 工作迴圈 中進行處理。如下是該迴圈的同步部分的實現:

function workLoop(isYieldy) { 
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);

}
} else {...
}
}複製程式碼

在上面的程式碼中,nextUnitOfWork 持有 workInProgress 樹中的 Fiber 節點的引用,這個樹有一些工作要做。當 React 遍歷 Fiber 樹時,它會使用這個變數來知曉是否有任何其他 Fiber 節點具有未完成的工作。處理過當前 Fiber 後,變數將持有樹中下一個 Fiber 節點的引用或 null。在這種情況下,React 退出工作迴圈並準備好提交更改。

遍歷樹、初始化或完成工作主要用到 4 個函式:

為了演示他們的使用方法,我們可以看看如下展示的遍歷 Fiber 樹的動畫。我已經在演示中使用了這些函式的簡化實現。每個函式都需要對一個 Fiber 節點進行處理,當 React 從樹上下來時,您可以看到當前活動的 Fiber 節點發生了變化。從視訊中我們可以清楚地看到演算法如何從一個分支轉到另一個分支。它首先完成子節點的工作,然後才轉移到父節點進行處理。

「譯」React Fiber 那些事: 深入解析新的協調演算法

注意,垂直方向的連線表示同層關係,而折線連線表示父子關係,例如,b1 沒有子節點,而 b2 有一個子節點 c1

在這個 視訊 中我們可以暫停播放並檢查當前節點和函式的狀態。從概念上講,你可以將「開始」視為「進入」一個元件,並將「完成」視為「離開」它。在解釋這些函式的作用時,您也可以在 這裡 使用示例和實現。

我們首先開始研究 performUnitOfWorkbeginWork 這兩個函式:

function performUnitOfWork(workInProgress) { 
let next = beginWork(workInProgress);
if (next === null) {
next = completeUnitOfWork(workInProgress);

} return next;

}function beginWork(workInProgress) {
console.log('work performed for ' + workInProgress.name);
return workInProgress.child;

}複製程式碼

函式 performUnitOfWorkworkInProgress 樹接收一個 Fiber 節點,並通過呼叫 beginWork 函式啟動工作。這個函式將啟動所有 Fiber 執行工作所需要的活動。出於演示的目的,我們只 log 出 Fiber 節點的名稱來表示工作已經完成。函式 beginWork 始終返回指向要在迴圈中處理的下一個子節點的指標或 null。

如果有下一個子節點,它將被賦值給 workLoop 函式中的變數 nextUnitOfWork。但是,如果沒有子節點,React 知道它到達了分支的末尾,因此它可以完成當前節點。**一旦節點完成,它將需要為同層的其他節點執行工作,並在完成後回溯到父節點。**這是 completeUnitOfWork 函式執行的程式碼:

function completeUnitOfWork(workInProgress) { 
while (true) {
let returnFiber = workInProgress.return;
let siblingFiber = workInProgress.sibling;
nextUnitOfWork = completeWork(workInProgress);
if (siblingFiber !== null) {
// If there is a sibling, return it // to perform work for this sibling return siblingFiber;

} else if (returnFiber !== null) {
// If there's no more work in this returnFiber, // continue the loop to complete the parent. workInProgress = returnFiber;
continue;

} else {
// We've reached the root. return null;

}
}
}function completeWork(workInProgress) {
console.log('work completed for ' + workInProgress.name);
return null;

}複製程式碼

你可以看到函式的核心就是一個大的 while 的迴圈。當 workInProgress 節點沒有子節點時,React 會進入此函式。完成當前 Fiber 節點的工作後,它就會檢查是否有同層節點。如果找的到,React 退出該函式並返回指向該同層節點的指標。它將被賦值給 nextUnitOfWork 變數,React將從這個節點開始執行分支的工作。我們需要著重理解的是,在當前節點上,React 只完成了前面的同層節點的工作。它尚未完成父節點的工作。只有在完成以子節點開始的所有分支後,才能完成父節點和回溯的工作。

從實現中可以看出,performUnitOfWorkcompleteUnitOfWork 主要用於迭代目的,而主要活動則在 beginWorkcompleteWork 函式中進行。在後續的系列文章中,我們將瞭解隨著 React 進入 beginWorkcompleteWork 函式,ClickCounter 元件和 span 節點會發生什麼。

commit 階段

這一階段從函式 completeRoot 開始。在這個階段,React 更新 DOM 並呼叫變更生命週期之前及之後方法的地方。

當 React 進入這個階段時,它有 2 棵樹和副作用列表。第一個樹表示當前在螢幕上渲染的狀態,然後在 render 階段會構建一個備用樹。它在原始碼中稱為 finishedWorkworkInProgress,表示需要對映到螢幕上的狀態。此備用樹會用類似的方法通過 childsibling 指標連結到 current 樹。

然後,有一個副作用列表 — 它是 finishedWork 樹的節點子集,通過 nextEffect 指標進行連結。需要記住的是,副作用列表是執行 render 階段的結果。渲染的重點就是確定需要插入、更新或刪除的節點,以及哪些元件需要呼叫其生命週期方法。這就是副作用列表告訴我們的內容,它頁正是在 commit 階段迭代的節點集合。

出於除錯目的,可以通過 Fiber 根的屬性 current訪問 current 樹。可以通過 current 樹中 HostFiber 節點的 alternate 屬性訪問 finishedWork 樹。

commit 階段執行的主要函式是 commitRoot 。它執行如下下操作:

  • 在標記為 Snapshot 副作用的節點上呼叫 getSnapshotBeforeUpdate 生命週期

  • 在標記為 Deletion 副作用的節點上呼叫 componentWillUnmount 生命週期

  • 執行所有 DOM 插入、更新、刪除操作

  • finishedWork 樹設定為 current

  • 在標記為 Placement 副作用的節點上呼叫 componentDidMount 生命週期

  • 在標記為 Update 副作用的節點上呼叫 componentDidUpdate 生命週期

在呼叫變更前方法 getSnapshotBeforeUpdate 之後,React 會在樹中提交所有副作用,這會通過兩波操作來完成。第一波執行所有 DOM(宿主)插入、更新、刪除和 ref 解除安裝。然後 React 將 finishedWork 樹賦值給 FiberRoot,將 workInProgress 樹標記為 current 樹。這是在提交階段的第一波之後、第二波之前完成的,因此在 componentWillUnmount 中前一個樹仍然是 current,在 componentDidMount/Update 期間已完成工作是 current。在第二波,React 呼叫所有其他生命週期方法和引用回撥。這些方法單獨傳遞執行,從而保證整個樹中的所有放置、更新和刪除能夠被觸發執行。

以下是執行上述步驟的函式的要點:

function commitRoot(root, finishedWork) { 
commitBeforeMutationLifecycles() commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();

}複製程式碼

這些子函式中都實現了一個迴圈,該迴圈遍歷副作用列表並檢查副作用的型別。當它找到與函式目的相關的副作用時,就會執行。

更新前的生命週期方法

例如,這是在副作用樹上遍歷並檢查節點是否具有 Snapshot 副作用的程式碼:

function commitBeforeMutationLifecycles() { 
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag &
Snapshot) {
const current = nextEffect.alternate;
commitBeforeMutationLifeCycles(current, nextEffect);

} nextEffect = nextEffect.nextEffect;

}
}複製程式碼

對於一個類元件,這一副作用意味著會呼叫 getSnapshotBeforeUpdate 生命週期方法。

DOM 更新

commitAllHostEffects 是 React 執行 DOM 更新的函式。該函式基本上定義了節點需要完成的操作型別,並執行這些操作:

function commitAllHostEffects() { 
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
} case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
} case Update: {
commitWork(current, nextEffect);
...
} case Deletion: {
commitDeletion(nextEffect);
...
}
}
}複製程式碼

有趣的是,React 呼叫 componentWillUnmount 方法作為 commitDeletion 函式中刪除過程的一部分。

更新後的生命週期方法

commitAllLifecycles 是 React 呼叫所有剩餘生命週期方法的函式。在 React 的當前實現中,唯一會呼叫的變更方法就是 componentDidUpdate

我們終於講完了。讓我知道您對該文章的看法或在評論中提出問題。我還有關於排程程式、子調和過程以及如何構建副作用列表的文章來對這些內容提供深入的解釋。我還計劃製作一個視訊,在這裡我將展示如何使用本文作為基礎來除錯應用程式。

文章可隨意轉載,但請保留此 原文連結。非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 caijun.hcj(at)alibaba-inc.com

來源:https://juejin.im/post/5c052f95e51d4523d51c8300

相關文章