React是一個用於構建界面的JavaScript庫。它的核心是跟蹤組件狀態變化並將更新後的狀態更新到屏幕上。在React中,咱們把這個過程稱爲 reconciliation (協調)。經過調用setState方法,React檢查狀態或屬性是否已更改,並在UI層上更新。html
首先看一個簡單的例子:node
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> ] } }
這是一個簡單的計數器的例子, 點擊按鈕,組件的狀態就會在處理程序中更新,組件的狀態的更新反過來會引發span元素的內容更新。react
下面是在協調階段,React內部會有各類活動。如下是計數器在協調階段所作的操做:git
協調期間還會執行其餘活動,例如調用生命週期方法,更新ref。這些活動在Fiber架構中統稱爲"work"(工做)。work的類型取決於React元素的類型。React元素有多種類型,好比類組件,函數組件,Portals,DOM節點等。而React元素類型則是由React.createElement的第一個參數決定。React.createElement函數在render建立元素中調用。github
<button key="1" onClick={this.onClick}>Update counter</button> <span key="2">{this.state.count}</span>
JSX在通過編譯後,會獲得以下的結果。這是render方法真正返回的結果算法
class ClickCounter { ... render() { return [ React.createElement( 'button', { key: '1', onClick: this.onClick }, 'Update counter' ), React.createElement( 'span', { key: '2' }, this.state.count ) ] } }
React.createElement函數會返回以下的結果:vim
[ { $$typeof: Symbol(react.element), type: 'button', key: "1", props: { children: 'Update counter', onClick: () => { ... } } }, { $$typeof: Symbol(react.element), type: 'span', key: "2", props: { children: 0 } } ]
而對於組件<ClickCounter>
的元素,它沒有props和key:數組
{ $$typeof: Symbol(react.element), key: null, props: {}, ref: null, type: ClickCounter }
在協調期間,render方法返回的React元素會被合併到Fiber節點樹之中。每個React元素都有對應的Fiber節點。與React元素不一樣,Fiber不會在每一次渲染的時候從新建立。Fiber會保存組件的狀態和DOM。架構
前面討論過,根據不一樣的React元素類型,會執行不一樣的活動。例如,對於Class組件會調用生命週期方法以及render方法。而對於DOM節點,它執行DOM mutation。所以,每一個React元素都被轉換爲相應類型的Fiber節點。節點描述了須要完成的"work"。app
能夠將Fiber節點看做一種數據解構,表示一個work單元。,Fiber架構還提供了一種跟蹤、調度、暫停和停止work的方法。
React會在首次將React元素轉換爲Fiber節點時,使用createFiberFromTypeAndProps
函數建立Fiber節點。在更新階段會複用Fiber節點,並使用React元素上的數據更新Fiber節點上的屬性。亦或者移動,刪除Fiber節點。
React源碼中的ChildReconciler函數包含了Fiber節點中全部的work
React會爲每個React元素建立一個Fiber節點,咱們會獲得一個Fiber節點樹
Fiber節點樹是經過鏈表的形式存儲的,每個Fiber都擁有child(第一個子節點的引用),sibling(第一個兄弟節點的引用)和return(父節點的引用),來表示層級關心。更多內容請參考這篇文章React Fiber爲何使用鏈表來設計組件樹
在第一次渲染完成後,React會生成一個Fiber樹。該樹映射了應用程序的狀態,這顆樹被稱爲current tree
。當應用程序開始更新時,React會構建一個workInProgress tree
, workInProgress tree
映射了將來的狀態。
全部的"work"都是在workInProgress tree
上的Fiber節點上進行的。當React開始遍歷current tree
時,它會爲每個現有的Fiber節點建立一個備份(alternate字段),alternate節點構成了workInProgress tree
。當全部更新和相關的"work"完成。workInProgress tree
會刷新到屏幕上。workInProgress tree
此時變爲了current tree
。
React的核心原則之一是"一致性", 它老是一次性更新DOM, 不會顯示部分結果. workInProgress就是一個用戶不可見的"草稿", React在它上面處理全部組件, 處理完成後將它再刷新到界面上.
在React的源碼中,有不少從workInProgress tree
和current tree
中獲取Fiber節點的函數。好比下面這個函數簽名
function updateHostComponent(current, workInProgress, renderExpirationTime) { ... }
workInProgress tree
的Fiber節點擁有current tree
對應節點的引用。反之亦然。
咱們能夠將React組件視爲使用state和props計算UI的函數。其餘的活動,好比手動修改DOM,調用生命週期都應該被視做一種反作用。在React的文檔中也說起了這一點
你以前可能已經在 React 組件中執行過數據獲取、訂閱或者手動修改過 DOM。咱們統一把這些操做稱爲「反作用」(side-effects),或者簡稱爲「做用」(effects)。由於它們會影響其餘組件,而且在渲染期間沒法完成。
大多數state和props的更新都會致使反作用。應用effects是一種work類型。所以Fiber節點是一種跟蹤更新和effects的便捷機制,每個Fiber節點都有與之相關聯的effects。它們被編碼在effectTag字段之中。
Fiber中的effects定義了處理更新以後須要作的"work"。對於DOM元素,"work"包含了添加,更新,刪除。對於類組件,包括了更新ref,調用componentDidMount和componentDidUpdate生命週期方法。還有其餘effects對應於其餘類型的Fibber。
React處理更新很是快。爲了達到更好的性能水平,採用了一些有趣的技術。其中之一就是將具備effects的Fiber節點,構建爲線性列表,以方便快速迭代。迭代線性列表要比迭代樹快的多,由於不須要迭代沒有side-effects的節點。
effects list的目的是是標記出具備DOM更新,或其它與之關聯的其餘effects的節點。effects list是finishedWork樹的子集。在workInProgress tree
和current tree
中使用nextEffect屬性連接在一塊兒。
丹·阿布拉莫夫(Dan Abramov)將Effects list提供了一個比喻。將Fiber想象成一顆聖誕樹,用聖誕燈將全部有效的節點鏈接在一塊兒。
爲了可視化這一點,讓咱們想象下面的Fiber樹,其中高亮顯示的節點有一些「work」要作。
例如,咱們的更新致使將c2插入到DOM中,d2和c1更改屬性,b2觸發生命週期方法。 Effects list列表將把它們連接在一塊兒,這樣React就能夠遍歷時跳過其餘節點。
能夠看到具備effects的節點如何連接在一塊兒。當遍歷節點時,React使用firstEffect指針肯定列表的起始位置。上圖的Effects list能夠用下圖表示
React應用都有一個或者多個充當容器的DOM元素
const domContainer = document.querySelector('#container'); ReactDOM.render(React.createElement(ClickCounter), domContainer);
React會爲容器建立FiberRoot對象,可使用容器的DOM引用訪問Fiber root對象:
// Fiber root對象 const fiberRoot = query('#container')._reactRootContainer._internalRoot
Fiber root是React保留對Fiber樹引用的地方,Fiber樹存儲在Fiber root對象的current屬性中
// Fiber樹 const hostRootFiberNode = fiberRoot.current
Fiber樹的第一個節點是一種特殊的類型節點,叫作HostRoot。它在內部建立,是最頂層組件的父組件。經過HostRoot節點的stateNode屬性能夠訪問FiberRoot節點.
// Fiber root對象 const fiberRoot = query('#container')._reactRootContainer._internalRoot // hostRoot const hostRootFiberNode = fiberRoot.current // true hostRootFiberNode.stateNode === fiberRoot
咱們能夠從HostRoot來訪問和探索整個Fiber樹。或者能夠經過組件的實例中得到單個Fiber節點
compInstance._reactInternalFiber
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元素的Fiber節點結構:
{ 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(記錄與之關聯的effects), nextEffect(鏈接具備effects的Fiber節點使其成爲線性節點)
保留對class組件實例的引用, DOM節點或其餘與Fiber節點相關聯的React元素類實例的引用。通常來講, 咱們能夠說這個屬性被用於保存與當前Fiber相關的本地狀態。
定義與此Fiber節點相關聯的函數或者類。對於class組件,type屬性指向構造函數。對於DOM元素,type屬性指向HTML標記。我常常用這個字段來判斷這個Fiber節點與那個元素相關。
定義Fiber節點的類型。在協調期間使用它肯定須要作的"work"。如以前所述"work"取決於React元素的類型。createFiberFromTypeAndProps函數將React元素映射成相對應的Fiber節點類型。
在咱們的例子中。ClickCounter的tag爲1,表示爲ClassComponent。span的tag爲5,標記爲HostComponent。
state更新和回調,DOM更新的隊列。
用於建立輸出Fiber的state。在處理更新的時候,它映射的是當前界面上呈現的state。
在上一次渲染過程當中用來建立輸出的Fiber props。
已經更新後的Fiber props。須要用於子組件和DOM元素。
一組children中的惟一標示。幫助React肯定那些發生了更改,新增或刪除。更詳細的解釋在這裏
完整的Fiber結構, 能夠在這裏看到,在上面的說明省略了不少的字段好比child,sibling並return。這三個字段是構成鏈表樹結構的關鍵。以及expirationTime、childExpirationTime和mode,這些字段是特定於Scheduler的。
React分兩個階段執行work:render(渲染)和 commit(提交)
在render(渲染)階段,React將更新應用於經過setState或React.render調度的組件, 並找出須要在UI中更新的內容。
若是是初始渲染,React將爲render方法返回的每個元素建立新的Fiber節點。在以後的更新中,將從新使用和更新現有的Fiber節點。
render階段會構建一個帶有side-effects(反作用)的Fiber節點樹。effects描述了下一個commit(提交)階段須要完成的「work」。在commit(提交)階段,React會使用標記有effects的Fiber節點並將其應用於實例上。遍歷Effects list執行DOM更新和其餘對用戶可見的更改。
請切記,render階段的工做是能夠異步執行的,React根據可用時間處理一個或者多個Fiber節點。當發生一些更重要的事情時,React會中止並保存已完成的工做。等重要的事情處理完成後,React從中斷處繼續完成工做。可是有時可能會放棄已經完成的工做,從頂層從新開始。此階段執行的工做是對用戶是不可見的,所以能夠實現暫停。可是在commit(提交)階段始終是同步的它會產生用戶可見的變化, 例如DOM的修改. 這就是React須要一次性完成它們的緣由。
調用生命週期函數使用React的「work」之一。在render階段調用這些生命週期方法:
因爲render階段不會產生DOM更新之類的反作用,所以React能夠異步地對組件進行異步處理更新(甚至可能在多個線程中進行)。可是帶有UNSAFE_前綴的生命週期函數經常會被誤用,開發者會把反作用添加到這些生命週期函數中。這可能會致使異步渲染出現問題
在commit階段調用這些生命週期方法,這些生命週期方法在commit階段執行,因此它們可能包含反作用並涉及DOM更新。
協調算法使用renderRoot函數從最頂層的HostRoot節點開始,跳過已經處理過的節點,直到找到work未完成的節點爲止。例如, 當在組件樹深處調用setState方法, React 從頂部開始快速的跳過全部父級節點直接得到調用setState方法的組件。
全部的Fiber節點在render階段都會在WorkLoop中被處理。這是循環同步部分的實現:
function workLoop(isYieldy) { if (!isYieldy) { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } else {...} }
在上面的代碼中,nextUnitOfWork保持了對workInProgress tree
中一個有工做要處理的Fiber節點的引用。在React遍歷Fiber樹時,會使用nextUnitOfWork判斷是否有未完成"work"的Fiber節點。當節點處理完成「work」後,nextUnitOfWork會指向下一個Fiber節點的引用或者爲null。當nextUnitOfWork爲null時,React會退出WorkLoop,並準備進入到commit階段。
有四個主要的方法用於遍歷樹,並啓動或完成工做:
爲了演示如何使用它們。請查看下面遍歷Fiber樹的演示動畫。演示動畫中使用了這些函數的簡化實現。咱們能夠經過演示看到,首先處理子節點的「work」,而後處理父節點的「work」。
使用直線鏈接表明同級,使用折線鏈接的代筆子級
逐步拆分下React遍歷Fiber樹的過程(首先處理子節點的「work」,而後處理父節點的「work」):
這個是視頻的鏈接, 從概念上將"begin"當作進入組件,「complete」當作離開組件。
咱們首先看下beginWork和performUnitOfWork這兩個函數:
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 tree中接收一個Fiber節點。而後調用beginWork開始處理Fiber節點的work。爲了演示,這裏只是log了Fiber節點的name字段表示work已經完成。函數beginWork老是返回指向循環中下一個子節點或null。
若是有下一個子節點, 它將在workLoop函數中分配給nextUnitOfWork。若是沒有子節點,React就知道了到達了分支的結尾。就會完成當前Fiber節點的work。React會執行它兄弟節點的工做,最後回溯到父節點。這是在completeUnitOfWork中完成的。
function completeUnitOfWork(workInProgress) { while (true) { let returnFiber = workInProgress.return; let siblingFiber = workInProgress.sibling; nextUnitOfWork = completeWork(workInProgress); if (siblingFiber !== null) { // 若是有同級,則返回它。以繼續執行同級的工做 return siblingFiber; } else if (returnFiber !== null) { // 回溯到上一級 workInProgress = returnFiber; continue; } else { // 已經到了root節點 return null; } } } function completeWork(workInProgress) { console.log('work completed for ' + workInProgress.name); return null; }
當workInProgress節點沒有子節點時,會進入此函數。在完成當前Fiber的工做後,會檢查是否有兄弟節點。若是有,返回同級的兄弟節點的指針,分配給nextUnitOfWork。React將會從兄弟節點開始工做。只有處理完子節點全部分支以後, 纔會回溯到父節點(全部子節點處理完成後,纔會回溯到父節點)。
從實現能夠看出,completeUnitOfWork主要用於迭代,主要工做都是beginWork和completeWork函數中進行的。
這裏是完整的示例(beginWork,performUnitOfWork,completeUnitOfWork,completeWork的簡易實現)
// 首先構建鏈表樹 const a1 = {name: 'a1', child: null, sibling: null, return: null}; const b1 = {name: 'b1', child: null, sibling: null, return: null}; const b2 = {name: 'b2', child: null, sibling: null, return: null}; const b3 = {name: 'b3', child: null, sibling: null, return: null}; const c1 = {name: 'c1', child: null, sibling: null, return: null}; const c2 = {name: 'c2', child: null, sibling: null, return: null}; const d1 = {name: 'd1', child: null, sibling: null, return: null}; const d2 = {name: 'd2', child: null, sibling: null, return: null}; a1.child = b1; b1.sibling = b2; b2.sibling = b3; b2.child = c1; b3.child = c2; c1.child = d1; d1.sibling = d2; b1.return = b2.return = b3.return = a1; c1.return = b2; d1.return = d2.return = c1; c2.return = b3; // 當前的指針是a1 let nextUnitOfWork = a1; workLoop(); // 開始工做循環 function workLoop() { while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } } function performUnitOfWork(workInProgress) { let next = beginWork(workInProgress); if (next === null) { next = completeUnitOfWork(workInProgress); } return next; } function beginWork(workInProgress) { log('work performed for ' + workInProgress.name); return workInProgress.child; } function completeUnitOfWork(workInProgress) { while (true) { let returnFiber = workInProgress.return; let siblingFiber = workInProgress.sibling; nextUnitOfWork = completeWork(workInProgress); if (siblingFiber !== null) { return siblingFiber; } else if (returnFiber !== null) { workInProgress = returnFiber; continue; } else { return null; } } } function completeWork(workInProgress) { log('work completed for ' + workInProgress.name); return null; } function log(message) { let node = document.createElement('div'); node.textContent = message; document.body.appendChild(node); }
提交階段從completeRoot開始。這是React更新DOM,調用getSnapshotBeforeUpdate,componentDidMount,componentDidUpdate,componentWillUnmount等生命週期的地方。
React進入這一階段時,有兩顆樹(workInProgress tree和current tree)以及effects list。current tree
表示了當前屏幕上呈現的狀態。render階段遍歷current tree
時會生成另外一顆樹,在源碼中被稱爲finishWork或workInProgress,表示將來須要在屏幕上呈現的狀態。workInProgress tree
和current tree
結構相似。
調試時,如何獲取current tree
以及workInProgress tree
?
// current tree // 從容器對象上獲取FiberRoot對象 const fiberRoot = query('#container')._reactRootContainer._internalRoot // 獲取current tree const currentTree = fiberRoot.current // 獲取workInProgress tree const workInProgressTree = fiberRoot.current.alternate
提交(commit)階段,主要執行commitRoot函數,執行如下的操做:
workInProgress tree
設置爲current tree
。在調用getSnapshotBeforeUpdate方法後,React將commit,Fiber樹中全部的反作用。分爲兩步:
第一步,執行全部的DOM插入,更新,刪除和ref卸載。而後將workInProgress tree
設置爲current tree
樹。這是在第一步完成以後,第二步以前完成的。所以在componentWillUnmount生命週期方法在執行期間,狀態依然是更新以前的。而componentDidMount/componentDidUpdate執行時的狀態是更新以後的。第二步,執行其餘生命週期方法和ref回調,這些方法做爲單獨的過程被執行。
commitRoot方法的預覽:
function commitRoot(root, finishedWork) { // 用來執行getSnapshotBeforeUpdate commitBeforeMutationLifecycles() // 用戶更新DOM,以及執行componentWillUnmount commitAllHostEffects(); root.current = finishedWork; // 調用componentDidUpdate和componentDidMount生命週期的地方 commitAllLifeCycles(); }
這些子函數,內部都包含了一個循環。循環遍歷effects list,並檢查effects的類型。當發現類型和子函數的目的相同時,就應用它。
遍歷effects list,並檢查節點是否具備Snapshot effect的源代碼:
function commitBeforeMutationLifecycles() { while (nextEffect !== null) { const effectTag = nextEffect.effectTag; if (effectTag & Snapshot) { const current = nextEffect.alternate; commitBeforeMutationLifeCycles(current, nextEffect); } nextEffect = nextEffect.nextEffect; } }
若是是class組件,調用getSnapshotBeforeUpdate生命週期方法。
commitAllHostEffects是React執行DOM更新的地方。React會把componentWillUnmount做爲commitDeletion刪除過程當中的一部分。
function commitAllHostEffects() { switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect); ... } case PlacementAndUpdate: { commitPlacement(nextEffect); commitWork(current, nextEffect); ... } case Update: { commitWork(current, nextEffect); ... } case Deletion: { commitDeletion(nextEffect); ... } } }
commitAllLifecycles是React調用全部剩餘生命週期方法componentDidUpdate和componentDidMount的地方。
React源碼很複雜,Max Koretskyi的這篇文章內容也不少,全部總結下這篇博客的要點:
current tree
。當開始更新時,React會構建一個workInProgress tree
。current tree
表明了當前的狀態,workInProgress tree
表明了將來的狀態。workInProgress tree
會被設置爲current tree
。current tree
時,會爲每個Fiber節點建立一個alternate字段,alternate字段保存了Fiber節點的備份,alternate字段上保存的備份Fiber節點構成了workInProgress tree
。query('#container')._reactRootContainer._internalRoot
。fiberRoot.current
。workInProgress tree
, fiberRoot.current.alternate
。workInProgress tree
設置爲current tree
樹。