(譯)Fiber 架構之於 React 的意義

原文地址javascript

背景知識

Fiber 架構主要有兩個階段:reconciliation / render(協調/渲染) 和  commit(提交階段)。在源代碼中  reconciliation 階段一般被劃歸爲 render階段。 這個階段 React遍歷組件樹執行一下工做:html

  • 更新 stateprops
  • 調用生命週期鉤子
  • 父級組件中經過調用 render 方法,獲取子組件 (類組件),函數組件則直接調用獲取
  • 將獲得的子組件與以前已經渲染的組件比較,也就是 diff
  • 計算出須要被更新的 DOM

上述全部活動都涉及到 Fiber 的內部工做機制。具體工做的區分執行則基於 React Element 的類型。例如,對一個類組件 Class Component 來講,React須要去實例化這個類(也就是 new Component(props)),然而對一個函數組件  Function Component 來講則沒有必要。 若是感興趣,在這裏能夠看到 Fiber中定義的全部工做目標類型。這些活動正是安德魯在演講中所提到了的。java

When dealing with UIs, the problem is that if too much work is executed all at once, it can cause animations to drop frames… 當處理UIs時,問題是若是大量的工做一次性執行,那麼就可能致使動畫掉幀……node

那麼,關於 ‘all at once’ 的部分指的是什麼呢?基本上能夠這樣認爲,若是 React 同步遍歷整個組件樹,那麼它須要爲每一個組件執行對應的數據渲染更新等工做。因而當組件樹較大時就會形成這部分代碼的執行時間超過 16ms ,進而致使瀏覽器畫質渲染掉幀,肉眼可見的卡頓感。react

那怎麼辦呢?git

Newer browsers (and React Native) implement APIs that help address this exact problem… 相對較新的瀏覽器都實現了一個新的API ,它能夠幫助解決這個問題……github

他提到的這個新的 API 是一個名字叫  requestIdleCallback 的全局方法,它能夠在瀏覽器空閒時間段內調用函數隊列。能夠這樣使用它:web

requestIdleCallback((deadline)=>{
    console.log(deadline.timeRemaining(), deadline.didTimeout)
});
複製代碼

若是你如今打開瀏覽器並還行上面的代碼,Chrome  的日誌將會打印  49.885000000000005 false 。基本上能夠認爲是瀏覽器告訴你,你如今有 49.885000000000005 ms 能夠作任何你須要去作事情,而且時間尚未用完。反之deadline.didTimeout 則是 true 。請記住, 一旦瀏覽器開始一些工做後,timeRemaining 就隨時被改變 。算法

requestIdleCallback is actually a bit too restrictive and is not executed often enough to implement smooth UI rendering, so React team had to implement their own version._ 實時上 requestIdleCallback 的限制太多,不能被屢次連續的執行來實現流暢的UI渲染,因此 React 團隊被迫實現一個本身的版本。數組

如今若是咱們將 React 在一個組件上須要執行的全部活動都集中在  performWork函數中,那麼若是使用   requestIdleCallback 處理調度工做的話,咱們的代碼應該是這樣的:

requestIdleCallback((deadline) => {
    // while we have time, perform work for a part of the components tree
    while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
        nextComponent = performWork(nextComponent);
    }
});
複製代碼

咱們在一個組件上執行相關工做,完成後會返回下一個將要被處理組件的引用。這也就是安德魯以前討論過的問題:

in order to use those APIs, you need a way to break rendering work into incremental units 爲了使用這些APIs,你須要一種方式將渲染工做分割爲一個個的增量單元。

爲了解決這個問題,React 被迫從新實現了一個新的算法,從基於內部棧調用的同步遞歸模型轉變爲基於鏈表指針異步模型。關於這部分安德魯曾寫過:

If you rely only on the [built-in] call stack, it will keep doing work until the stack is empty…Wouldn’t it be great if we could interrupt the call stack at will and manipulate stack frames manually? That’s the purpose of React Fiber. Fiber is re-implementation of the stack, specialized for React components You can think of a single fiber as a virtual stack frame. 若是你依賴於內部調用棧,那麼它將一直工做直到棧被清空……,若是咱們能夠暫停調用棧,而且去改變它,這樣豈不是很棒。這其實就是 Fiber 的最終目的。Fiber 是一個從新實現的棧,主要針對 React 組件。你甚至能夠吧單個 fiber 看作是一個棧的虛擬幀。

這將是我接下來要解釋的。

關於棧的一些知識

我猜大家對棧概念應該不陌生。若是在代碼中打上斷點,在瀏覽器中運行,這時大家將看到它。這裏有一段來自維基百科的相關描述:

In computer science, a call stack is a stack data structure that stores information about the active subroutines of a computer program… the main reason for having call stack is to keep track of the point to which each active subroutine should return control when it finishes executing… A call stack is composed of stack frames… Each stack frame corresponds to a call to a subroutine which has not yet terminated with a return. For example, if a subroutine named DrawLine is currently running, having been called by a subroutine DrawSquare, the top part of the call stack might be laid out like in the adjacent picture. 在計算機科學中,一個調用棧其實就是一個存儲着計算機程序中的活動子程序相關信息的數據結構……,主要緣由是調用棧能夠追蹤活動子程序在完成執行後返回的控制位置……,一個調用棧由多個棧數據幀組成……,每一個數據幀對應一個尚未執行結束的子程序的調用。例如,一個叫 DrawLine 的子程序正在運行,它是以前被子程序DrawSquare 所調用,因而這個棧頂的結構相似於下面這張圖片。

_

stack.png


棧 和 React 到底什麼關係

像咱們文章以前提到了,React 在遍歷整個組件樹期間須要爲組件執行相關更新對比等操做。React 以前所實現的協調器使用的是基於瀏覽器內部棧同步遞歸模型。這裏有官方提供的文檔對這部分的描述以及遞歸相關的討論:

默認狀況下,當遞歸一個 DOM 節點的孩子節點的時候,React 將會同時遍歷兩個孩子列表,當有任何不一樣的時候就會生成新的改變後的孩子節點。

仔細想一想,每一次的遞歸調用都會添加一個數據幀到棧中。並且這整個過程都是同步的。假如咱們有下面的組件樹:

componentTree.png

使用對象來代替 render 函數。你甚至能夠認爲它們是組件樹的實例。

const a1 = {name: 'a1'};
const b1 = {name: 'b1'};
const b2 = {name: 'b2'};
const b3 = {name: 'b3'};
const c1 = {name: 'c1'};
const c2 = {name: 'c2'};
const d1 = {name: 'd1'};
const d2 = {name: 'd2'};

a1.render = () => [b1, b2, b3];
b1.render = () => [];
b2.render = () => [c1];
b3.render = () => [c2];
c1.render = () => [d1, d2];
c2.render = () => [];
d1.render = () => [];
d2.render = () => [];
複製代碼

React 須要迭代這個組件樹,而且爲每一個組件執行一些工做。爲了簡單起見,這部分工做被設計在日誌中打印當前組件的名稱和檢索它的孩子組件。

遞歸遍歷

迭代這個這棵樹的主函數叫 walk , 下面是它的實現:

walk(a1);

function walk(instance) {
    doWork(instance);
    const children = instance.render();
    children.forEach(walk);
}

function doWork(o) {
    console.log(o.name);
}
複製代碼

獲得的輸出結果:

a1, b1, b2, c1, d1, d2, b3, c2
複製代碼

遞歸方法直覺上是很適合遍歷整個組件樹。可是咱們發現它其實有必定的侷限性。最重要的一點是它不能將要遍歷的組件樹分割爲一個個的增量單元來處理,也就是說不能暫停遍歷工做在某個特殊的組件上,然後繼續。React 會遍歷整個組件樹直到棧被清空爲止。

so,那麼 React 在不使用遞歸的狀況下如何實現遍歷算法呢?實際上它使用了一種單鏈表遍歷算法,它讓遍歷暫停成爲一種可能。

鏈表遍歷

爲了實現這個算法,咱們須要一種數據結構,它包含一下 3 個  fields

  • child —— 第一個孩子節點的引用
  • sibling —— 第一個兄弟節點的引用
  • return —— 父節點的引用

React 中新的協調算法的上下文就是一種被稱爲 Fiber 的數據結構,它必須包含上面提到的 3 個 fields。它的底層實現是經過 React Element 提供的數據來建立一個一個的 Fiber 節點。

下面的圖表展現了 fiebr node 鏈接起來的層次結構,經過鏈表和之間的屬性相互鏈接:

fiber.png


如今咱們開始定義本身的 Node 結構。

class Node {
    constructor(instance) {
        this.instance = instance;
        this.child = null;
        this.sibling = null;
        this.return = null;
    }
}
複製代碼

下面這個函數是拿到一個 nodes 數組,而後將他們鏈接起來。咱們要用它去鏈接被 render 方法返回的節點。

function link(parent, elements) {
    if (elements === null) elements = [];

    parent.child = elements.reduceRight((previous, current) => {
        const node = new Node(current);
        node.return = parent;
        node.sibling = previous;
        return node;
    }, null);

    return parent.child;
}
複製代碼

這個函數會從數組的最後一個元素開始迭代並將他們鏈接起來造成一個單鏈表。它會返回列表中第一個節點的引用。下面的 demo 用來演示它是如何工做的:

const children = [{name: 'b1'}, {name: 'b2'}];
const parent = new Node({name: 'a1'});
const child = link(parent, children);

// the following two statements are true
console.log(child.instance.name === 'b1');
console.log(child.sibling.instance === children[1]);
複製代碼


這裏咱們還要實現一個工具函數,它可協助節點執行一些工做。在咱們的例子中,它將在日誌中打印輸出組件的名字。除此以外它還會取出該組件的孩子組件並將它們 鏈接起來:

function doWork(node) {
    console.log(node.instance.name);
    const children = node.instance.render();
    return link(node, children);
}
複製代碼

ok,如今咱們能夠實現核心遍歷算法了。它本質上是一個深度優先算法:

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
        // 爲當前的 node 執行一些工做,並將它與其孩子節點鏈接起來,並返回第一個孩子節點引用
        let child = doWork(current);

        // 若是孩子節點存在,則將它設置爲接下來 被 doWork 方法處理的 node
        if (child) {
            current = child;
            continue;
        }

        // 若是已經返回到更節點說明遍歷完成,則直接退出
        if (current === root) {
            return;
        }

        // 當前節點的兄弟節點不存在的時候,返回到父節點,繼續尋找父節點的兄弟節點,以此類推
        while (!current.sibling) {

            // 若是已經返回到更節點說明遍歷完成,則直接退出
            if (!current.return || current.return === root) {
                return;
            }

            // current 指向父節點
            current = current.return;
        }
        
		// current 指向兄弟節點
        current = current.sibling;
    }
}
複製代碼

若是咱們查看上面的算法的實如今調用棧中的狀況,相似於下圖:

callstack.gif

想必你也看到了,遍歷整個組件樹的時候我棧並無疊加。可是若是咱們在  doWork 函數中 加上 debugger  的時候,咱們會看到下面的過程:

stacklog.gif

它幾乎和瀏覽起得調用棧相同。因此使用咱們本身實現的這個算法能夠有效的代替瀏覽器實現的調用棧。這其實就是安德魯所描述的:

對 React 組件來講 ,Fiber 是棧的從新實現。你甚至能夠認爲單個 Fiber 就是一個虛擬棧 frame。

咱們經過保留 node 的引用(棧頂),來控制整個棧。

function walk(o) {
    let root = o;
    let current = o;

    while (true) {
            ...

            current = child;
            ...
            
            current = current.return;
            ...

            current = current.sibling;
    }
}
複製代碼

咱們能夠再任什麼時候候通知遍歷操做,也能夠在隨後從新啓動。這就是咱們想要的,接下來就可以使用新的 requestIdleCallback API 來實現調度工做了。

React 中的工做循環

這裏能夠看到 React 中循環的具體實現:

function workLoop(isYieldy) {
    if (!isYieldy) {
        // Flush work without yielding
        while (nextUnitOfWork !== null) {
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
    } else {
        // Flush asynchronous work until the deadline runs out of time.
        while (nextUnitOfWork !== null && !shouldYield()) {
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
    }
}
複製代碼

你能夠看到,它的實現很好的對應了上面講到的算法。它老是將 current Fiber Node 的引用保存到 nextUnitOfWork 變量中,用來扮演棧頂。

一般在出現交互式 UI 更新的狀況下, 如(click,input,等等)時,該算法會同步遍歷整個組件樹,爲每一個 Fiber Node 執行相關工做。固然它也能夠異步遍歷,正在執行 Fiber Node 完成後,會檢查是否還有時間剩餘。shouldYield 函數基於 deadlineDidExpire 和 deadline 這兩個變量計算並返回 true or false ,用於決定是否暫停遍歷。這兩個變量在 React 遍歷執行工做過程當中也是不斷被更新改變的。

總結

譯者添加

Fiber 架構對於 React 簡單的來講能夠理解爲讓 React 擺脫了瀏覽器棧的約束,能夠根據瀏覽器的空閒狀態選擇是否執行渲染工做,就總體渲染時間來講並無縮短,反而是拉長了,整個渲染工做被劃分爲多個過程,這些過程都分散在瀏覽器的各個空閒時間段內,就 UI 而言,對用戶來講視覺上會更加流暢。

相關文章
相關標籤/搜索