原文引用:in-depth overview of the new reconciliation algorithm in React
做者:Max Koretskyijavascript
React
是一個用於構建用戶界面的 JavaScript
庫。它的核心原理是追蹤組件中的狀態的改變,而後將這些被更新的狀態自動刷新到屏幕上。在 React
中有一個過程被稱之爲 協調 reconciliation
。也就是當咱們調用 setState
方法,或是框架檢測到 state
or props
變化後,便開始從新開始計算比對組件的先後狀態,並渲染與之對應的改變的 UI。
React 官方文檔提供了對其原理的高階概述:React Element
,生命週期方法,render
方法,組件孩子節點的 diff
算法的應用等 在 React
中所扮演的角色。其中 render 方法所返回的 React elements Tree
就是咱們經常提到的 虛擬DOM virtual DOM
。這個術語有助於前期向人們解釋 React
,可是它也會形成一些困惑,由於任何 React
的官方文檔中都沒出現過它。因此在這篇文章中,我將堅決稱它爲 React elements
樹。
除之外,React 還有另一棵內部實例樹(組件實例,或 DOM 節點等),它被用來保存 state 狀態。從 React16 開始 React 推出了新的內部實例樹的實現 以及它的管理算法 統稱爲 Fiber
。
html
下面是一個很是簡單的小應用,接下來的整篇文章都將使用到它。一個按鈕,點擊數字加一,並渲染到屏幕上。java
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
方法中返回了兩個元素 button
和 span
。當你快速點擊按鈕的時候,組件的狀態會在內部更新。這時 span
的文本內容也隨之更新。node
在 React 協調過程當中有各類各樣的工做須要被執行。以上面代碼爲例,在 React 第一次渲染和隨後的 state 更新期間的作了一些事情,大體以下:react
ClickCounter
組件 state
的 count
屬性。ClickCounter
組件的孩子節點並對比他們的 props
。span
元素。還有一些在協調階段執行的工做,如:生命週期方法調用或 refs 更新等。全部這些活動在 Fiber
架構中被總稱爲 ‘work’
。‘work’
的類型一般基於 React Element
的 type
。 例如,對類組件來講須要 React 來建立實例,相對於函數組件來講就沒有必要了。衆所周知,在 React 中有很的組件類型,類組件,函數組件(無狀態組件),宿主組件(DOM),portals
等。React Element
的 type
由 createElement 函數的第一個參數定義。這個函數一般在 render
方法中被使用,用於建立上面提到的 React elements
。git
在咱們探索 ‘work’
和 Fiber
架構算法以前,咱們先熟悉下一在 React 內部使用的數據結構。github
在 React
中每個組件都有其對應的 UI 呈現,咱們稱之爲視圖,或者也能夠說是 render
方法返回的模板。例子 ClickCounter
組件的模板以下:算法
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
複製代碼
當一個模板被傳入到 JSX
編譯器,最終會生成 React Element
。它其實就是 React
組件的 render
方法返回的實際內容,並非 HTML..,固然咱們也能夠不使用 JSX
語法,也能夠直接像下面代碼示例中展現那樣寫組件 ,能接受的話,哈哈哈……:json
class ClickCounter {
//...
render() {
return [
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
]
}
}
複製代碼
這裏調用的 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](https://overreacted.io/why-do-react-elements-have-typeof-property/)
屬性,它在後會用到,主要是用來甄別是否爲合法有效的 Element
。同時咱們可看到了type
, key
and props
三個屬性。這裏咱們須要要注意一下 React
是如何表示 span
和 button
兩個節點的文本內容的,還有 button
節點的 onClick
部分。
對於示例組件 **ClickCounter**
來講,它自身的 React Element
並無添加任何屬性 或者 key
。
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
複製代碼
協調其實是將從組件 render
方法返回的每一個 React Element
合併進入fiber node
樹的過程。每個 React Element
都有一個對應的 fiber
節點。不一樣於 React Element
,fiber
並不會在每次渲染時候都從新建立。它們是可變的數據結構,保存着組件的狀態,以及對應實體 DOM
節點的引用,這個下面會講到。
咱們以前提到過,React
會基於 React Element
的 type
屬性執行不一樣工做。在示例程序中, ClickCounter
組件將調用生命週期函數 和 render
方法,而宿主組件 span
將會改變 DOM
內容。每個 React Element
都會轉換爲它所對應的 Fiber Node
,用於描述接下來要被執行的工做。
也就是說,能夠認爲 fiber
節點就是描述隨後要執行工做的一種數據結構,能夠類比於調用棧中的幀 ,換而言之,就是一個工做單元。fiber
架構同時還提供了便捷的方式去追蹤,調度,暫停,中斷協調進程。
當一個 React Element
第一次被轉換爲 fiber
節點的時候,React
將會從 React Element
種提取數據子並在在 createFiberFromTypeAndProps
函數中建立一個新的 fiber
。隨後的更新過程當中 React
會複用這個建立的 fiber
節點,並將對應 React Element
被改變數據更新到這個 fiber
節點上。React 也會移除一些 fiber
節點,例如:當同層級上對應 key
屬性改變時,或 render
方法返回的 React Element
沒有該 fiber
對應的 Element
對象時,該 fiber
就會被刪除。
由於 React
會爲每一個獲得的 React Element
建立 fiber
,這些 fiber
節點被鏈接起來組成 fiber tree
。在咱們的例子中它是這樣的:
全部的 fiber
節點都經過屬性 child
, sibling
and return
鏈接起來。
在第一次渲染完成後,React
最終生成 fiber tree
,它能夠理解爲渲染出的 UI 界面的在內存中的映射。fiber tree
的引用變量一般是 current
。當 React
開始更新操做時,它會又會構建一棵被稱爲 workInProgress
的新樹。workInProgress
接下來將會替換 current
變量所引用的舊的 fiber tree
,而後隨之會被刷新到屏幕上。
全部執行相關更新,刪除等操做的 fibers
都來自於 workInProgress
樹。當 React 遍歷 current
樹的時候,會爲每一個 fiber node
建立一個備用節點,這些備用節點最終組成整個 workInProgress
樹。這些被新建立的 fibers 的數據來自於 render
方法返回的 React Element
。一旦更新操做的工做完成,React
將會擁有一顆隨時即可刷新到屏幕的備用樹。當 workInProgress
樹被刷新到屏幕後,那麼它就會變成 current
樹。也就是說 current
變量會保存這顆新樹的指針。
React
的一個核心原則就是一致性。因此 React
會一次性遍更新全部須要處理的 DOM
,不會只顯示部分結果。 workInProgress tree
對用戶而言相似於一個不可見的草稿。因此 React 會先處理全部的組件內部狀態的更新,而後再將它們改變了的部分從新刷新到屏幕上。
在源碼中你會看到不少的函數從 current
and workInProgress
樹 中獲取 fiber
節點。每個 fiber
節點在 alternate
域中保存它在另外一棵樹中對應節點的引用。也就是說一個來自 current
樹的節點會指向 workInProgress
樹中相對應的節點,反之亦然。
咱們能夠認爲一個 React
組件,它其實就是一個使用 state
和 props
來計算 UI 表現形式的函數。像 DOM
或 調用生命週期方法這樣的活動都被認爲是一個反作用。關於反作用的詳情能夠查看官方文檔。
平常開發中,其實不少時候 state 和 props 的更新都會致使反作用的產生。每種反作用的應用其實是一種指定類型的工做,而 fiber
節點是一種方便的機制去追蹤反作用,從它被添加直到被應用。每個 fiber node
都有會有與之相關的反作用。它們都被編碼在fiber
節點的 effectTag
域上。
因此,Fiber
中的反作用基本上能夠理解爲,定義了在更新進程完成後須要對實例執行的具體操做。對宿主組件而言,也就是 DOM
,的這些工做包括 element
的添加,更新,移除。對的類組件來講可能須要更新 refs
以及調用生命週期方法等。
React
的 DOM
更新速度很是快,爲了實現這樣的性能,它的實現上採用了一些有趣的技術,其中之一就是構建了一個能夠高效迭代遍歷的線性 fiber node
列表,該列表中全部 fiber node
都是有反作用須要被執行的,對於沒有反作用的 fiber node
則沒必要浪費寶貴的時間了。
這個列表的目的是標記那些須要執行各類反作用的節點,包括 DOM
更新,刪除,生命週期函數調用等。它同時也是 finishedWork
樹的子集,列表中的節點之間經過 nextEffect
屬性鏈接。
Dan Abramov 對反作用列表作了一個有趣的比喻,他認爲反作用列表像一顆聖誕樹,全部的反作用節點被導線綁在一塊兒。以下圖:
React
經過
firstEffect
指針獲得列表的起點。因此上面的圖也能夠理解爲這樣:
React
應用反作用的順序是從子節點到父節點。
每個 React
應用都有一個或多個 DOM
元素做爲容器,一般開發中那個 id
爲 root
的元素。示例程序中的容器是 id
爲 container
的 div
。
const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
複製代碼
React 會爲每個容器建立一個 fiber root
對象。 進入這個地址能夠看到它的具體實現。
const fiberRoot = query('#container')._reactRootContainer._internalRoot
複製代碼
fiber root
是 React
保存 fiber tree
引用的地方。也就是 fiber root
節點上的屬性 current
。
const hostRootFiberNode = fiberRoot.current
複製代碼
fiber tree
起點是一個被稱之爲 HostRoot
的特殊類型的 fiber node
。它在內部被建立,扮演着全部節點的祖先節點的角色。HostRoot
的屬性 stateNode
反過來又指向 FiberRoot
。
fiberRoot.current.stateNode === fiberRoot; // true
複製代碼
你能夠探索你本身應用的中的 fiber tree
經過進入頂層的 fiberRoot
,進而獲得 HostRoot
。或者你也能夠經過組件實例獲得單個 fiber
節點。
compInstance._reactInternalFiber
複製代碼
如今讓咱們瞥一眼建立的 ClickCounter
組件的 fiber node。
{
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 HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
複製代碼
fiber
節點上有不少的域,alternate
, effectTag
和 nextEffect
這幾個域的用處在以前的部分已經講解過了,如今讓咱們開始研究剩下的這些。
用於保存類組件的實例,宿主組件的 DOM 實例等。一般咱們也能夠說這個屬性是用來保存與該 fiber
相對應的的本地狀態 。
定義了與該 fiber node
相對應的是一個函數組件仍是一個類組件。若是是一個類組件該屬性指向這個類的構造函數。若是是一個 DOM 元素,該屬性則是與之相對應的 HTML
標籤。使用這個域很容易就能理解與該 fiber
節點相關聯的元素是什麼。
定義了當前 fiber
的類型。協調算法用它來判斷具體須要作什麼工做。像以前說的這個工做的類型仍是基於 React Element
的類型。createFiberFromTypeAndProps
函數映射一個 React Element 到與之相對應的 fiber node 類型。在咱們的示例中, ClickCounter
組件的屬性 tag
是 1,表示 ClassComponent
, span
元素的 tag 是5 ,表示 HostComponent
。
一個狀態更新隊列,包括回調 和 DOM
更新。
已經被使用渲染的過的 fiber
狀態。也就是當前屏幕上 UI 狀態的映射。
已經使用渲染過的 fiber
屬性,也是構成當前屏幕 UI 狀態映射的一部分。
保存着最近一次從 render
方法返回的 React Element
中拿到的數據,等待隨後被應用到子組件或是 DOM
元素上。
相同層級孩子節點惟一標記,能夠優化提高 React 對子節點更新,添加,刪處的判斷效率。與它具體功能相關的官方文檔能夠看這裏。reactjs.org/docs/lists-…
React 工做執行大體能夠分爲兩個階段:render
和commit
。
在 render
階段,React 經過調度 setState
或者 React.render
來實現組件更新,同時也會計算出須要被更新的那部分 UI 的狀態。若是是是初始化渲染,React
會爲 render
方法返回的每個 React Element 建立一個 fiber 對象。在隨後的更新中,若是當時建立 fiber
時對應的 Reatc Element
還存在,那麼該 fiber
還會被複用,或者更新。這個階段最後的成果就是在這顆 fiber
樹上對標記了出了那些有反作用的 fiebr
節點。在接下來的 commit
階段就是處理這些被標記節點反作用的節點 ,最後呈現爲可視化的 UI。
有一件很重要的事情須要理解,那就是 render
階段的執行能夠是異步的。React 會處理一個或多個 fiber
節點 ,基於可利用的有效時間,若是有效時間用完它就會停下來,亦或者讓位於優先級更高的事件,例如:用戶點擊操做等。隨後再次找到它暫停的位置處繼續它未完成的工做。但有時,則須要放棄已經執行過的工做,而後從頭開始。這些暫停之因此成爲多是由於它們執行的工做並不會形成用戶可見的改變,像 DOM 更新之類的。相反,接下來的 commit
階段則都是同步執行的。由於這個階段全部執行的工做是反作用的應用,最主要的DOM 更新,會致使用戶可見的改變。這就是爲何 React
須要一次性執行完全部反作用的緣由。
調用生命週期方法也是反作用的一部分。一些方法在 render
階段被調用,剩下的固然在 commit 階段被調用啦。下面列舉了在 render
階段調用的生命週期方法:
從列表中你能夠看到,不少在 render
階段被調用的舊的生命週期方法在 version 16.3
及後面的版本中都被標記上了 UNSAFE
。如今在這篇文檔中它們被統稱爲遺留生命週期方法。它們頗有可能會在將來的版本中被移除。
想知道緣由嗎?
好吧,其實這其中的緣由與咱們上面學到的知識息息相關,首先咱們知道 render
階段的更新不會有任何影響用戶視覺上可見的的反作用產生,例如: DOM
更新之類的,甚至 React
還會異步的更新組件。然而這些被使用 UNSAFE
標記的生命週期方法常常會被開發者錯誤的使用,甚至濫用。開發者傾向於將包含反作用的代碼放置到這些方法中,這樣就可能致使新的異步渲染被觸發,例如:在 componentWillUpdate
函數中調用 setState
方法就會致使錯誤產生。甚至程序無限循環,直至奔潰。UNSAFE
標記也是提醒開發者慎重使用。
接下來咱們講講 commit
階段被調用的生命週期方法:
由於這些方法的執行的執行都是同步的,因此它們能夠包含不少的反作用以及 DOM
操做。
ok,目前咱們已經掌握了足夠的背景知識,接下來就能夠潛入探究一番, React
用到的遍歷和執行相關操做的算法。
協調算法的執行老是從頂層的 HostRoot
節點開始執行工做,經過調用 renderRoot
方法開始。然而 React
會跳過那些被處理過的 fiber
節點,直到找到還未被處理的節點。例如,若是你在組件樹的某一處調用了 setState
,React
會從頂部開始遍歷,可是會快速的跳過它的祖先節點,直到找到觸發 setState
的組件爲止。
全部的 fiebr
節點都在在循環遍歷中被處理。咱們先看看循環算法實現的同步部分:
function workLoop(isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {...}
}
複製代碼
代碼中的 nextUnitOfWork
變量保存着從 workInProgress
樹中取出須要被處理的 fiebr
節點的引用。當 React 遍歷這個 Fiber
樹的時候,它可使用這個變量的引用來不斷檢索遍歷到剩下還未被處理的 fiber
節點。在當前的 fiber
節點被處理後,這個變量隨後會指向下一個要被處理的 fiber
節點的引用,存在的話。不然爲 null
。
在遍歷樹和初始化的過程當中重要用的的 4 個方法:
爲了說明他們是如何被使用的,請看下面的 fiber
樹遍歷的示意動畫:
注意:垂直方向上節點之間經過 siblings 屬性鏈接,折線處表示鏈接孩子節點,經過 children 屬性。
讓咱們首先從 performUnitOfWork
和 beginWork
這兩個方法開始。
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;
}
複製代碼
performUnitOfWork
方法接受一個來自 workInProgress
樹的 fiber
節點做爲參數,同時調用 beginWork
方法。一個 fiber
節點的全部須要執行的處理都開始於這個方法。爲了證實這一點,咱們簡單的在日誌中記錄那些已經完成功能處理的 fiber 節點的名字。 beginWork
方法老是會返回一個指向下一個將要被處理的孩子節點的指針,或者 null
。
若是存在下一個孩子節點,那麼它將在循環中被賦值給 nextUnitOfWork
變量。然而要是返回了 null
,這時 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
就會進入這個函數。當完成當前遍歷到的孩子節點的工做後,React 就檢查是否有兄弟節點,若是有 React
會退出這個函數,並返回該節點兄弟節點的指針。它一樣會被賦值給 nextUnitOfWork
變量,節點來 React
會重複上面的過程,直至整棵子樹被遍歷處理完成。當子節點以及子節點的孩子節點都被處理完成後,回溯至父節點,再重複循環父節點的兄弟節點,直至整棵樹被遍歷完成,最終返回到根節點 fiberRoot
。
這個階段開始於 completeRoot
函數。在這個裏 React
會更新 DOM
,調用相關生命週期方法。
當 React 進入這個階段,它有兩棵樹和反作用列表。第一棵樹就是是當前已經刷新到屏幕上 UI 對應的狀態。另外一顆備用樹就是在 render
階段構建的,在源碼中它一般稱之爲finishedWork
或 workInProgress
,在接下來的 commit
階段會替換以前的舊樹,將新的狀態刷新到屏幕上。
finishedWork
樹的上經過 nextEffect
指針鏈接的 fiber
節點構成反作用列表。_反作用列表能夠看作是 render
階段運行產生的成果。_渲染的意義就是去決定節點的插入,更新,刪除,或是組件生命週期函數的調用。這些就是反作用列表將要告訴咱們的,也是接下來提交階段須要遍歷的節點集合。
在提交階段運行的主函數是 commitRoot
,基本上它作了以下工做:
Snapshot
反作用的節點上調用 getSnapshotBeforeUpdate
生命週期方法。Deletion
反作用的節點上調用 componentWillUnmount
生命週期方法。DOM
插入,更新,刪除操做。current
指針指向 finishedWork
樹。Placement
反作用的組件節點上調用 componentDidMount
生命週期方法。Update
反作用的組件節點上調用 componentDidUpdate
生命週期方法。在 getSnapshotBeforeUpdate
調用後,React
會提交整棵樹的全部反作用。整個過程分爲兩步。第一步執行 DOM
插入,更新,刪除,ref
的卸載。接下來 React
將finishedWork
賦值給 FiberRoot
,並標記 workInProgress
樹爲 current
樹。這樣作的緣由是,第一步至關因而 componentWillUnmount
階段,current
指向以前的樹,而接下里的第二步則至關因而 componentDidMount/Update
階段,current
要指向新樹。
上面描述的主要執行函數:
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
生命週期方法。
React 執行DOM 更新使的是 commitAllHostEffects
函數。
function commitAllHostEffects() {
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
...
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
commitWork(current, nextEffect);
...
}
case Update: {
commitWork(current, nextEffect);
...
}
case Deletion: {
commitDeletion(nextEffect);
...
}
}
}
複製代碼
很是有趣,在 commitDeletion
函數中 React 調用 componentWillUnmount
是方法做爲刪除處理的一部分。
commitAllLifecycles
函數中 React 調用了全部剩下的生命週期方法,componentDidUpdate
, componentDidMount
譯者添加
協調過程實際就是遍歷整個 Fiber
樹的時候,經過從 React Element
中獲取到的改變後的數據,而後將這些數據更新到其所對應的 fiber
節點上,不存在對應的fiber
節點時,則建立新的,而後經過數據計算判斷出該 fiber
節點在 commit
階段須要作的事情,添加上對應的 effectTag
,同時該節點也會被添加到反作用列表中。在遍歷完成以後會生成一棵新Fiber
樹,該樹中的 fiber
節點一些是新建立的,一些則是複用 old fiber tree
中的,具體狀況取決於返回的 React Element
。在 commit
階段就是遍歷反作用列表並執行 effectTag
標記的工做。