深刻React Fiber架構的reconciliation 算法

本文爲意譯和整理,若有誤導,請放棄閱讀。 原文javascript

前言

本文將會帶你深刻學習React的新架構-Fiber和新的reconciliation 算法的兩個階段。咱們將會深刻探討一下React更新state和props,處理子組件的具體細節。html

正文

React是一個用於構建用戶界面的javascript類庫。它的核心機制是對組件狀態進行變動檢測,而後把(存在內存中的)已更新的狀態投射到用戶界面上。在React中,這個核心機制被稱之爲reconciliation。咱們調用setState方法,而後React負責去檢測state或者props是否已經發生變動,最後把組件從新渲染在屏幕上。java

React文檔爲這個機制給出了一個很高質量的闡述。react element在這裏的角色,生命週期函數,render方法和應用到子組件身上的diffing算法等方面的闡述包羅萬象。從組件的render方法返回的不可變的react element樹被大衆稱爲「virtual DOM」。在React發展的早期,這個概念有助於React團隊向人們去解釋React的運行原理,可是到了後來,人們發現這個概念過於模糊,容易讓人產生歧義。故,在React文檔中再也不使用過這個概念了。本文中,我堅持把它叫回「react element tree」(譯者注:react element tree從另一個角度來看,它也是一個react element)。node

除了這個react element tree以外,React在內部也維持着一顆叫作「internal instance tree」(這個instance又對應着component實例或者DOM對象)。這顆樹是用於保存整個應用的狀態的。從React的v16.0.0開始,React對這個「internal instance tree」做了一個新的實現。而用於管理這顆樹的算法就是如雷貫耳的Fiber。若是你想要了解Fiber架構所帶來的好處,請戳這裏:在Fiber架構中,React爲何使用和如何使用單鏈表react

這是【深刻xxx】系列中的第一篇文章,它志幫助你去了解React的內部架構。在本文中,我將會深刻講解一些跟算法相關的,重要的概念和數據結構。一旦咱們掌握了這些概念和數據結構,咱們就能夠探索整個算法和一些遍歷,處理fiber node tree過程當中所用到的主要函數。在這個系列中的下一篇文章中,我將會闡述React是如何應用這個算法去完成界面的的初始渲染和處理state,props的更新。後續,咱們將會繼續探索scheduler的實現細節,child reconciliation 的處理流程和構建effect list的機制。git

我會向你輸出一些真正高級的知識?是的。我鼓勵你去閱讀這系列的文章,去了解React Concurrent特性的背後的魔法。我堅信逆向工程(reverse-engineering)的好處,因此,我會給出不少可以跳轉到Reactv16.6.0源碼的連接。github

整篇文章下來,要接受的東西確實太多了。因此,在你不能立刻理解一個東西的時候,千萬不要焦慮。你須要給點耐心,花點時間去理解它,由於這都是十分值得的。注意,你不須要掌握任何React在應用實踐方面的知識。這篇文章主要是講React的內部運行原理算法

背景交代

我將會用一個簡單的應用示例貫穿整個系列。這個示例是這樣的:咱們有一個button,經過點擊這個button,咱們能夠增長界面上一個數字的值,以下圖:vim

下面是它的實現代碼:bash

lass 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>
        ]
    }
}
複製代碼

你能夠在這裏玩玩它。正如你看到的代碼那樣,ClickCounter是一個簡單的組件。這個組件有兩個子元素:buttonspan。它們都是從ClickCounter組件的render方法中返回的。當你點擊界面上的button的時候,內部的事件處理器就會更新組件的狀態。從而最終致使span元素更新它的文本內容。

React在reconciliation過程當中會執行各類各樣的activity。好比說,在本示例中,如下就是React在首次渲染和在狀態更新後會執行的主要操做:

  • 更新ClickCounter組件的state對象的count屬性;
  • 獲取(經過調用render方法來獲取)和比對ClickCounter最新的children和它們的props;
  • 更新span元素的textContent屬性值。

除此以外,在reconciliation期間,React還有不少別的activity要執行。好比說,調用生命週期函數啊,更新refs啊等等。全部的這些activity在Fiber架構中都被稱之爲「work」。不一樣類型的react element(react element的類型靠其對象的「type」字段來指示)通常有着不一樣類型的work。比方說,對於class component而言,React須要建立它的實例對象。而對於functional component而言,React不須要這麼作。正如你所知道的那樣,在React中,咱們有着各類類型的react element。好比說,咱們有class component,functional component, host component和portal等等。react element的類型是由咱們調用createElement函數時所傳遞進入的第一個參數所決定的。而createElement函數就是組件render方法用於建立react element的那個函數。

在咱們開始探索各類各類的work和fiber架構的主要算法以前,讓咱們先來熟悉熟悉React內部所用到的數據結構。

從react element到fiber node

React中的每個組件都有一個相應的UI representation。它們就是從組件的render函數返回的東西。咱們姑且稱之爲「view」或者「template」。下面就是咱們示例ClickCounter組件的「template」:

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

react element

一旦一個template被JSX compiler編譯事後,咱們將會獲得大量的react element。

更嚴謹點說,template被JSX compiler編譯事後是先獲得一個被wrap到render方法裏面函數調用。只有render方法被調用了,咱們才能得獲得的element。

而這些react element纔是組件render方法所返回的真正的東西。本質上來講,render方法所返回的東西既不是HTML標籤,也不是「template」,而是一個【返回react element的】函數調用。「template」,「HTML標籤」或者更嚴格得說「JSX」只是外在模樣,咱們根本不須要用它們來表示。下面是用純JavaScript來重寫的ClickCounter組件:

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

ClickCounter組件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爲這些js對象都添加上了一個【用於惟一標識它們是react element的】屬性:$$typeof。除此以外,咱們還有用於描述react element的屬性:typekeyprops。這些屬性值都是來自於你調用React.createElement函數時傳入的參數。值得關注的是,React是如何表示span和button節點的文本類型的子節點的,click事件處理器是如何成爲props的一部分的。固然,react element身上還有一些其餘的屬性,好比「ref」字段。不過,它們不在本文的討論範圍內。

ClickCounter組件所對應的react element沒有任何的props和key:

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

fiber node

在reconciliation(發音:【ˌrekənsɪliˈeɪʃn】)期間,組件render方法所返回的react element身上的數據都會被合併到對應的fiber node身上,這些fiber node也所以組成了一顆與react element tree相對應的fiber node tree。 須要緊緊記住的是,每個react element都會有一個與之對應的fiber node。跟react elment不同的是,fiber node不須要在每一次界面更新的時候都從新建立一遍(譯者注:react element偏偏相反。每一次都會被從新建立一遍)。fiber node是一種用於保存組件狀態和DOM對象的可變數據結構

咱們先前提到過,針對不一樣類型的react element,React須要執行不一樣的activity。在咱們這個示例中,對於ClickCounter而言,這個activity就是調用它的生命週期方法和render方法。而對於span這個host component而言,這個activity就是執行DOM操做。每一個react element都會被轉換成一個相應類型的fiber node。不一樣類型的work標誌着不一樣類型的fiber node。

你能夠把fiber node看作一種普通的數據結構。只不過,這種數據結構表明的是某種須要去完成的work。也能夠換句話說,一個fiber node表示一個work單元。Fiber架構也同時提供了一種便利的方式去追蹤,調度,暫停和中斷work。

createFiberFromTypeAndProps函數中,React利用了來自於react element身上的數據完成了對從react element到fiber node的首次轉換。在隨後的更新中,對於一些依舊存在的react element而言,React不會從新建立而是複用以前的fiber node。React僅僅在必要的時候,利用其對應的react element身上的數據去更新fiber node身上的相關屬性。同時,React也須要根據key屬性去調整fiber node在樹上的層級或者當render方法再也不返回該fiber node所對應的react element的時候把這個fiber node刪除掉。

你能夠在 ChildReconciler 函數中看到全部的activity和用於處理已存在的fiber node的相關函數。

在沒引入fiber架構以前,咱們已經存在一顆叫react element tree的東西。在引入fiber架構後,由於React會爲每個react element去建立一個與之相對應的fiber node。因此,咱們也就有了一顆與react element tree相對應的fiber node tree。在咱們這個 示例中,這顆fiber node tree長這樣的:

正如你說見的那樣,全部的fiber node經過childsiblingreturn鏈成了一個linked list。若是你想知道爲何這種設計是行得通的,你能夠閱讀個人另一篇文章 The how and why on React’s usage of linked list in Fiber

Current tree和 workInProgress tree

在application的首次渲染以後,React會生成一整顆的fiber node tree用於保存【那些已經用於渲染用戶界面的】狀態(也就是react組件的state)。這顆樹通常被稱之爲current tree。當React進入更新流程後,它又構建了另一顆叫作workInProgress tree的節點樹。這顆樹保存着那些即將會被flush到用戶界面的應用狀態。

全部在workInProgress tree上的fiber node的work都會被執行完成。當React遍歷current tree的時候,它會爲這顆樹上的每個fiber node建立一個稱之爲alternate fiber node的節點。正是這種節點組成了workInProgress tree(譯者注:也就是說,workInProgress tree就是alternate fiber node tree)。React經過利用render方法所返回的react element身上的數據來建立了alternate fiber node。一旦全部的更新請求(調用setState一次能夠視爲一次更新請求)都被處理完畢,全部的work也執行完畢的話,那麼React就產出了一顆用於將全部變動flush到用戶界面的alternate fiber node tree。在將這顆alternate fiber node tree映射到用戶界面後,它就變成了current tree

React的核心準則之一是:一致性(consistency)。React老是一口氣地完成DOM更新。這就意味着它不會向用戶展現更新到一半的用戶界面。workInProgress tree做爲一個「draft」而被用於React的內部,用戶是看不到的。因此,React可以先處理完全部的組件,最後,才把須要變動的東西flush到用戶界面上。

在React的源碼中,你會看到不少的函數實現都是從current treeworkInProgress tree同時讀取它們的fiber node。下面,就是一個這樣的函數的簽名:

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

current treeworkInProgress tree上的對等的fiber node都會經過一個alternate字段值來保存着一個【指向其對等fiber node的】引用。(譯者注:二者處於一種循環引用的狀態,僞代碼表示以下:)

const currentNode = {};
 const workInProgressNode = {};
 
 currentNode.alternate = workInProgressNode;
 workInProgressNode.alternate = currentNode;
 
複製代碼

Side-effects

咱們能夠把React component理解成一個接受state和props做爲輸入,最終計算出一個UI representation的函數。全部其餘的activity,例如:更改DOM結構,調用組件的生命週期方法等等咱們均可以考慮將其稱爲「side-effect」,或者簡稱爲「effect」。在React的這篇官方文檔中也提到effect的定義:

你以前頗有可能作過相似於data fetching, subscription或者手動更改DOM結構諸如此類的操做。由於這些操做會影響到其餘組件,同時它們都不能在rendering期間(譯者注:此處的「rendering期間」就是「render階段」)去完成的。因此,咱們將這些操做稱之爲「side effect」(或者簡稱爲effect)

你將會看到大部分的state和props的更新都會致使side effect的產生。而又由於應用effect也是一種類型的work。因此,fiber node是一種很好的【,用於去追蹤除了update以外的effect的】機制。每個fiber node均可以帶有effect。effect是經過fiber node的effectTag字段值來指示的。

因此能夠這麼說,一個fiber node被更新流程處理事後,它的effect基本上就定義這個fiber node【須要爲對應組件實例所要作的】work。具體點說,對於host component(DOM element)而言,它們的work能夠包含「增長DOM元素」,「修改DOM元素」和「刪除DOM元素」等。而對於class component而言,它們的work能夠包含「update refs」,「調用componentDidMount和componentDidUpdate生命週期函數」。對於其餘類型的fiber node而言,還有其餘的work存在。

Effects list

React處理更新流程的速度很是快。爲了實現這個性能目標,React應用了幾個有趣的技巧,其中之一就是:爲了加快迭代的速度,React爲那些帶有effect的fiber node構建了一個linear list。其中的緣由是由於迭代linear list比迭代一顆tree的速度要快得多。對於那些沒帶有effect的fiber node,咱們更沒有必要花時間去迭代它。

這個linear list的目標把須要進行DOM操做或者有其餘effect相關聯的fiber node標記出來。跟current treeworkInProgress tree中的fiber node是經過child字段將彼此連接一塊兒不一樣,這個linear list中的fiber node經過自身的nextEffect字段來把彼此連接起來的。它是finishedWork tree的子集。

Dan Abramov 曾經對「effect list」作個一個類比。他把fiber node tree比喻成一棵聖誕樹。而聖誕樹上把小燈飾鏈接起來,纏繞着聖誕樹的那些電線就是咱們的「effect list」。爲了可視化去理解它,讓咱們一塊兒想象下面這顆fiber node tree中的顏色高亮的節點是帶有work的。舉個例子,咱們的更新流程將會致使c2被插入打DOM中,d2c1將會改變自身的attribute,b2將會調用自身的生命週期方法等等。那麼,這顆fiber node tree的effct list將會把這些節點鏈接到一塊,這樣在,React在遍歷的時候可以作到跳過其餘的fiber node:

從上面的圖示,咱們能夠看到帶有effect的fiber node是如何連接到一塊的。當React遍歷這些節點的時候,React會使用firstEffect指針來指示出list的第一個元素。因此,上面的圖示能夠精簡爲如下的圖示:

Root of the fiber tree

每個React應用都有一個或者多個DOM元素充當着container的角色。在本示例中,有着id值爲「container」的div元素就是這種container。

const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);
複製代碼

React爲每個container都建立了fiber root。你能夠經過DOM元素身上保存的引用來訪問它:

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

這個fiber root就是React保存fiber node tree引用的地方。fiber root是經過currnet的屬性值來保存這個引用的:

const hostRootFiberNode = fiberRoot.current
複製代碼

fiber node tree以一個特殊類型的fiber node開頭。這個fiber node被稱之爲HostRoot。它是React內部建立的,被當作是你最頂層組件的父節點。同時,經過HostRootstateNode字段,咱們能夠回溯到FiberRoot身上來:

fiberRoot.current.stateNode === fiberRoot; // true
複製代碼

你能夠經過訪問最頂層的HostRoot來探索整一顆fiber node tree。又或者,你能夠經過訪問一個組件實例的_reactInternalFiber屬性來訪問某一個單獨的fiber node:

const fiberNode = compInstance._reactInternalFiber
複製代碼

Fiber node structure

下面,讓咱們一塊兒來看看,本示例中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元素的fiber node:

{
    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 node這種數據結構身上仍是挺多字段的。我已經在先前的章節中解釋過alternateeffectTagnextEffect這幾個字段的用途了。下面,咱們來看看爲啥還須要其餘字段。

stateNode

這個字段保存着組件實例,DOM對象等react element type的引用。 通常狀況下,咱們能夠說這個字段保存着fiber node所關聯的local state。

type

這個字段定義了與當前fiber node相關的function或者class。對於class component而言,該字段值就是這個class的constructor函數。而對於DOM元素而言,該字段值就是這個DOM元素所對應的就是字符串類型的HTML標籤名。我常常用這個字段去理解一個fiber node所關聯的react element是什麼樣子的。

tag

這個字段定義了fiber node的類型。在reconciliation算法中,它被用於決定一個fiber node所須要完成的work是什麼。早前咱們提到過,work的具體內容是由react element的類型(也就是type字段)來決定。createFiberFromTypeAndProps函數負責將某個類型的react element轉換爲對應類型的fiber node。具體落實到本示例中,ClickCounterfiber node的tag值是「1」,這就表明着這個fiber node所對應的react element是ClassComponent類型 。對於span fier node而言,它的tag值是「5」。這就表明着它所對應的react element是HostComponent類型。

updateQueue

A queue of state updates, callbacks and DOM updates.

memoizedState

已經被用於建立output的fiber node state。若是咱們當前是在處理流程中, 那麼fiber node的該字段保存的就是那些已經被渲染到用戶界面的狀態。

memoizedProps

已經被用來在上一次渲染期間建立output的fiber node props。

pendingProps

當前渲染期間,已經被更新的了,等待被應用到child component或者DOM元素身上的fiber node props。fiber node props的更新是經過利用來自於react element中的數據來完成的。

key

惟一標識一個children列表中的每個item。它被用於幫助React去計算出哪一個item被更改了,哪一個item是新添加過來,哪一個item被移除了。React在這篇General algorithm文檔中對它(指的是key字段)更加具體的做用做出了很好的闡述。

小結

你能夠在這裏找到fiber node完整數據結構的說明。在本文中,我已經跳過了不少的字段了。須要特別提到的是,本文中沒有說起的,用於將各個fiber node連接成樹狀結構的childsiblingreturn字段,其實我已經在先前的這篇文章中說明過了。而歸屬於其餘分類的字段,好比,expirationTimechildExpirationTimemode等字段都是跟Scheduler相關的。

General algorithm(通用算法)

React執行work的過程分爲兩個階段:render階段commit階段。

在render階段,當用戶調用setState()或者React.render()方法的時候,React就會對組件實施更新操做,而後計算出整個用戶界面中須要更新的地方。若是是組件初次掛載的render階段,React會爲每個【從組件render方法返回的】react element建立與之對應的fiber node。在下一次的render階段裏面,若是某個fiber node所對應的react element還存在的話(譯者注:在每一次更新中,都會調用render方法。拿返回的react element跟以前的react element判斷,就知道該react element是否還存在),那麼這個fiber node將會獲得複用和更新。render階段的最終目的是產出一顆標記好effect(是否帶有effect,effect的類型是什麼)的fiber node tree。一個fiber node的effec字段是用於描述在接下來的commit階段,這個fiber node須要完成的work有哪些。在這個commit階段,React接收一棵標記好effect的fiber node tree做爲輸入,而後將它應用到其對應的實例上。具體點來講,就是遍歷effect list,根據相應的effect去執行相應的work:DOM更新或者其餘結果對用戶可見的操做。

須要明白的一點是,render階段的work的執行能夠是異步的。取決於可用的時間,React能夠處理一個或者多個fiber node,須要讓步給其餘事情的時候,React就暫停處理,將手頭上已經完成的work暫存起來。而後,從它上次中斷的地方繼續執行。也不老是如此,有時候React仍是會放棄掉已經完成的work,從頭(譯者注:這個「頭」就是fiber node tree的第一個節點)開始作起。之因此可以暫停執行work是因在render階段執行的work並不會對用戶產生可見的界面效果,好比說,DOM更新就會產生可見的界面效果。於此相反,在接下來的commit階段老是同步執行的。那是由於這個階段須要執行的work是會對用戶產生可見的界面效果的,因此,React會一口氣完成這個流程(指commit階段)。

「調用生命週期方法」是React須要執行的一種work。其中,一部分的生命週期方法會在render階段被調用,而另一部分會在commit階段調用。下面是render階段會調用的生命週期方法:

  • [UNSAFE_]componentWillMount (deprecated)
  • [UNSAFE_]componentWillReceiveProps (deprecated)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate (deprecated)
  • render

正如你所看的那樣,一些以「UNSAFE」爲前綴的,已經被遺棄的(legacy)生命週期方法(Reactv16.3版本引入此更改)會在render階段執行。如今,這些方法在React的官方文檔中都被稱之爲「遺棄的生命週期方法」。這些方法將會在某個16.x的版本中被廢棄(deprecated)。而它們的孿生方法,沒有以「UNSAFE」爲前綴的那些生命週期方法將會在17.0中被移除(removed)。你能夠在這篇文檔中看到這方面變更的介紹和API遷移方面的說明。

你是否是對這個變動背後的緣由感到好奇呢?

好吧,在這裏,我給你說道說道。正如咱們前面所說的那樣,由於render階段,React不會產出(produce)effect,好比DOM更新之類的。同時,React可以對組件進行異步的更新(甚至有可能以一種多線程的方式去作)。然而,那些以「UNSAFE」爲前綴的生命週期方法在實際生產中常常被誤解和誤用。開發者常常在這些生命週期方法裏面放置一些有(side-)effect的代碼。引入fiber架構後,React也爲咱們帶來了異步渲染的方案。若是開發者繼續這麼作的話,程序是會出問題的。儘管他們的孿生方法(沒有以「UNSAFE」爲前綴的那些)最終會被移除掉,可是他們依然可能會在將來的Concurrent Mode(你也能夠選擇不開啓Concurrent Mode)特性版本中產生問題。

下面是commit階段會執行到的生命週期方法:

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由於這些方法被執行的階段是同步執行的,因此,它們能夠包含side-effect代碼和訪問DOM元素。

好吧,講到這裏,咱們已經能具有足夠的知識儲備去理解【被用於遍歷fiber node tree和執行work的】generalized algorithm

Render phase

reconciliation老是從fiber node tree最頂端的HostRoot節點開始執行。這個開始動做發生在renderRoot函數裏面。然而,React會跳過那些已經處理過的fiber node,只會處理那些有帶有未完成work的節點。舉個例子說,若是你在組件樹的深層去調用setState方法的話,那麼React雖然仍是會從頂部的節點開始遍歷,可是它會跳到前面全部的父節點,徑直奔向那個調用了setState方法的子節點。

Main steps of the work loop

全部的fiber node都會在一次的work loop中獲得處理。下面是work loop同步執行部分的代碼實現:

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {...}
}
複製代碼

在上面的代碼中,nextUnitOfWork保存着一個指向workInProgress tree樹上某個還有未完成work的fibe node的引用。當React在這顆fiber node tree上遍歷的時候,它就是使用這個變量來判斷是否還有別的fiber node須要處理。當前fiber node一旦被處理後,nextUnitOfWork`變量的值要麼是指向下一個fiber node,要麼就是null。一旦是null的話,React就會退出work loop,而後準備進入commit階段。

有四個函數是用於遍歷fiber node tree,初始化或者完成work的:

它們是如何被使用的呢?讓咱們看看下面React遍歷fiber node tree時的動畫。爲了演示,咱們使用了這些函數的精簡版實現。這四個函數都接受一個fiber node做爲輸入。隨着React對這顆樹的往下遍歷,你能夠看到當前active的fiber node(橙色節點表明active的fiber node)在不斷地更改。你從這個動畫上清晰地看到這個算法是如何從fiber node tree的一個分支切換到另一個分支的。具體點說就是,先處理完全部children的work再回溯到parent節點(譯者注:其實就是深度優先遍歷):

請注意,上圖中,豎線是表明sibling關係,橫折豎線表明着children關係。例如:b1就沒有children,而 b1有一個children叫作c1

這裏是一個相關的視頻連接。在這個視頻裏面,你能夠暫停和回放,而後仔細瞧瞧,當前的fibe node是誰,當前這個函數的狀態是怎樣的。理論上說,你能夠把「begin」理解爲「stepping into」一個組件,而「complete」就是從這個組件中「stepping out」。你也能夠玩玩這個demo。在這個demo中,我解釋了這幾個函數具體都作了什麼。這個demo的代碼以下:

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;

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) {
            // 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 returnFiber. workInProgress = returnFiber; continue; } else { // We've reached the root.
            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);
}
複製代碼

下面一塊兒瞧瞧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;
}
複製代碼

performUnitOfWork函數以一個來自於workInProgress tree的fiber node做爲輸入,經過調用beginWork函數來開始執行work。React將會在beginWork函數裏面開始去完成一個fiber node全部須要被完成的work。爲了簡化演示,我把所須要完成的work假設爲:打印當前fiber node的名字。beginWork 函數要麼返回一個指向下一個work loop須要處理的fiber node的引用,要麼返回null。

若是有下一個待處理的child fiber node的話,那麼這個fiber node就會在workLoop函數裏面將它賦值給變量nextUnitOfWork。不然的話,React 知道已經觸達了當前(fiber node tree)分支的葉子節點了。所以,React能夠結束當前fiber node(的work)了。一旦一個fiber node被結束掉,React接着會執行它的sibling節點的work,在完成這個sibling的這個分支後,就會移步到下一個sibling節點.....以此類推。當全部的sibling節點被結束到,React纔會回溯到parent節點。。這個過程是發生在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 is no more work in this returnFiber,
            // continue the loop to complete the parent.
            workInProgress = returnFiber;
            continue;
        } else {
            // We have reached the root.
            return null;
        }
    }
}

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

正如你所看到的那樣,completeUnitOfWork的函數實現主體是一個大while循環。當workInProgress node沒有children的時候,React的執行就會進入這個函數。在完成當前fiber node的work以後,它就緊接着去檢查是否還有下一個sibling節點要處理。若是有的話,那麼就把下一個sibling的引用返回出去,退出當前函數。所返回的sibling的引用會賦值給nextUnitOfWork變量,而後React就從這個sibling開始新一輪的遍歷。值得強調的一點是,上面所提到的這個節骨眼上,React只是完成了先前sibling節點的work,它並無完成parent節點的work。當一個fiber node本身和其全部子節點分支上的work都被完成了,咱們才說這個fiber node的work完成了,而後才往回追溯。

正如你所看到的那樣,performUnitOfWorkcompleteUnitOfWork主要是用於對fiber node tree進行迭代。迭代中具體須要乾的事都是靠beginWorkcompleteWork去實現的。在本系列中接下里的文章中,咱們會了解到隨着React執行到beginWorkcompleteWork函數,ClickCounter組件和span組件到底發生了什麼。

Commit phase

這個階段以completeRoot函數開始。在這個函數裏面,React完成了DOM更新和對pre-mutaion和post-mutation生命週期方法的調用。

進入commit階段後,React須要跟三個數據結構對象打交道:兩棵tree,一個list。兩顆tree分別是指current treeworkInProgress tree(或者稱爲finishedWork tree),一個list是指effect listcurrent tree表明着當前已經渲染在用戶界面的應用的狀態。workInProgress tree是React在render階段構建出來的,視爲current tree的alternate。它表明着那些須要被flush到用戶界面的應用狀態。workInProgress treecurrent tree同樣,也是經過fiber node的child和sibling字段將本身連接起來的。

effect list是finishedWork tree的一個子集。它是經過fiber node的nextEffect字段將本身連接起來的。再次提醒你,effect list是render階段的產物。render階段所作的一切都是爲了計算出哪一個節點須要被插入,哪一個節點須要被更新,哪一個節點須要被移除,哪一個組件的生命週期方法須要被調用。這就是effect list能告訴咱們的信息。effect list上面的fiber node才真正是commit階段須要被遍歷到的節點。

debug的時候,你能夠經過fiber rootcurrent屬性值來訪問current tree。你能夠經過HostFiber節點的alternate屬性來訪問finishedWork tree上對應的fiber node。詳細能夠查看Root of the fiber tree這一小節。

commit階段的主要功能函數是commitRoot。能夠這麼說,它作了下面這些事:

  • 對帶有snapshoteffect的fiber node,調用它的getSnapshotBeforeUpdate生命週期方法
  • 對帶有Deletioneffect的fiber node,調用它的componentWillUnmount生命週期方法
  • 執行全部的DOM操做:節點插入,節點更新,節點刪除
  • current指針指向finishedWork tree(在javascript裏來講,就是引用傳遞)。
  • 對帶有Placementeffect的fiber node,調用它的componentDidMount生命週期方法
  • 對帶有Updateeffect的fiber node,調用它的componentDidUpdate生命週期方法

在調用完pre-mutation方法getSnapshotBeforeUpdate後,React會將樹上全部的effect commit掉。這個操做又分爲兩步走。第一步是:執行全部的DOM節點的插入,更新,刪除和ref的卸載。而後,React會把finishedWork tree賦值給FiberRoot節點的current屬性,以此把finishedWork tree轉換爲current tree。This is done after the first pass of the commit phase, so that the previous tree is still current during componentWillUnmount, but before the second pass, so that the finished work is current during componentDidMount/Update. 第二步:React調用全部的其餘生命週期方法和ref callback。由於這些方法都是在一個獨立的步驟裏面執行的。到這個時候,樹上全部的placements,updates和deletionseffect都已經被應用過了。

下面是commitRoot函數中上面提到兩個執行步驟:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}
複製代碼

全部的這些子函數都實現了對effect list的迭代。在迭代的過程當中,它們都會檢查當前fiber node的effect是不是本函數須要處理類型,若是是,則應用該effect。

Pre-mutation lifecycle methods

如下是一個小示例,裏面的代碼實現了對effect list的遍歷,而且在遍歷的過程當中去檢查當前的fiber node的effect type是不是Snapshot

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

對一個class component而言,「應用這個snapshot」effect「等同於「調用getSnapshotBeforeUpdate生命週期方法」;

DOM updates

React會在commitAllHostEffects 函數裏面完成全部的DOM操做。這個函數羅列DOM操做的類型和具體的操做:

unction 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方法的調用劃分到deletion這一類別中,最終在commitDeletion函數中調用了它。

Post-mutation lifecycle methods

剩下還有componentDidUpdatecomponentDidMount這兩個生命週期方法。它們會在commitAllLifecycles 裏面被調用。

結束語

最終的最終,咱們終於講完了。若是你想說出你對這篇文章的看法又或者想問問題,歡迎評論評論。同時也歡迎查閱個人下一篇文章:深刻react的state和props更新。在我打算寫完的這一【深刻xxx】系列中,我手頭上還有不少正在寫的文章。這些文章囊括了「scheduler」,「children reconciliation」和「effects list是如何構建起來的」等方面的主題。同時,我也打算基於本文所講內容發佈一個講解何如debug的視頻,歡迎翹首以盼。

相關文章
相關標籤/搜索