原文地址:The how and why on React’s usage of linked list in Fiber to walk the component’s treejavascript
React中,改變檢測一般被看做是協調(reconciliation)或者渲染(rendering),而Fiber正是這個機制的一種新的實現。在這個架構之下,能夠實現一些有趣特性,如:改善非阻塞渲染,執行基於優先級的更新,以及在後臺提早渲染內容。這些特性在併發React哲學中被認爲是time-slicing。除了解決開發者一些真實問題外,這些機制的內部實現,從工程角度來看也具備普遍的吸引力。關於其中原因的有價值的知識點將有助於咱們做爲開發者獲得成長。html
若是你如今Google查詢「ReactFiber」,你會搜到大量的相關文章,這些除了Andrew Clark的筆記都是至關高層面的講解。本篇文章中我將引用這個資源,而且提供一個細緻的關於Fiber中一些特別重要概念的講解。當咱們結束時,你將對Lin Clark在ReactConf 2017上關於工做方法(work loop)的演講有足夠的知識來理解,這個演講你須要看一下,不過在你大體看完以後,我將讓你有更多的理解。一篇解析一系列關於React Fiber內幕的文章,我大概花了70%的時間用來理解其內部的詳細實現,過程當中還寫了三篇關於協調和渲染機制的文章。java
咱們開始吧。node
Fiber的架構有兩個主要階段:協調/渲染(reconciliation/render)和提交(commit)。在源碼中協調階段基本上被看成是「渲染階段(render phase)」。這個階段中,React會遍歷組件樹而且會:react
全部這些操做在Fiber中被認爲是一個work。須要操做的work的類型取決於React Element的類型,例如,對於一個Class Component
React須要實例化一個類,但對於Function Component
則不須要。若是感興趣,你能夠在這裏看到Fiber中全部work對象的類型。這些操做確實如Andrew演講中所提到的:git
當處理一些UI時,有一個問題是,若是一次性執行太多的操做,那麼將會致使動畫掉幀github
那「一次性」指的是什麼呢?通常來講,React會同步遍歷整個組件樹,而且執行每一個組件的work,而執行它邏輯的時間可能超出了16ms。這便致使之了掉幀,繼而引發視圖卡頓。web
那,這有什麼辦法能夠解決嗎?算法
現代瀏覽器(包括React Native)實現了一些API有助於解決這個問題數組
一個新的全局方法的API叫 requestIdleCallback 能夠添加一些方法,而這些添加的方法將在瀏覽器閒置時間時被執行。你怎麼能夠本身使用一下呢?若是我在Chrome的console面板,執行如上代碼,會打印出49.9和false。這表示我能夠有49.9ms來執行想要作的事情,且我沒有用完分配的時間,不然deadline.didTimeout
就是true了。記住,只要瀏覽器有一些工做須要作,那麼timeRemaing
就會變化,須要不斷地檢測它。
requestIdleCallback確實有點使用限制,且不老是充分地執行來保證平滑的UI渲染,因此React團隊必須實現本身的一個版本。
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);
}
});
複製代碼
咱們在一個組件上執行work,而後返回下一個待繼續執行組件的引用。若是不是隻處理一件事的狀況下,這種方式是有效的。你不能夠同步處理整個組件樹,就像以前React關於協調算法的實現。這就如Andrew演講中所提到的問題:
爲了使用這些APIs(即requestIdleCallback),你須要一種方式,將渲染方式(rendering work)打破成可遞增的單元
因此爲了解決這個問題,React必須得從新實現遍歷組件樹的方法:從依賴內建調用棧來同步遞歸模式,換成使用鏈表和指針的異步模式。這即是Andrew所寫的:
若是你只是依賴內建的調用棧,那它將一直執行直到棧爲空。若是咱們能夠按照需求打斷調用棧,並手動維護棧幀,這樣不就最好了。這就是React Fiber的目的,Fiber則是特別針對React組件來從新實現的棧,你也能夠認爲一個fiber就是一個虛擬的棧幀。
這就是我如今講解的內容。
假設你對調用棧的感念比較熟悉,當你在瀏覽器調試工具中斷點時就能夠看到它,這裏是來自Wikipedia的引用和示例圖:
在計算機科學中,一個調用棧是棧的數據結構,用於保存計算機程序中活躍子程序的信息。設計調用棧的主要緣由是爲了跟蹤每個活躍子程序的引用,以便子程序執行結束時能夠返回控制權。一個調用棧是有一些棧幀組成的,每一個棧幀對應的就是每一個尚未結束的持有返回的子程序。例如,一個叫
DrawLine
的子程序正在執行,尚未被子程序DrawSquare
調用,那這個調用棧的頂層部分的構成就像以下圖片所示。
正如這篇文章第一部分中所說,React在協調/渲染階段遍歷組件樹,並在組件上執行一些操做,以前的協調算法是依賴內建調用棧的同步模式來遍歷樹。關於這個協調算法的官方文檔 描述了這個過程,且談及許多關於遞歸:
默認狀況下,當遞歸DOM節點的子節點時,React會在同一時間遍歷全部子節點列表,並由任什麼時候間產生的一個diff計算出一個突變。
想想,每次遞歸調用會在棧上添加一幀,且這個過程是同步的。假設咱們有以下組件樹:
以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使用這個方式就保持遍歷直處處理完全部的組件以及遞歸棧爲空。
那麼,React如何不使用遞歸來遍歷組件樹的呢?它使用了單鏈表遍歷樹算法,這樣就能夠暫停遍歷且阻止棧的增加。
我在這裏找到Sebastian Markbåge關於該算法的大體說明。爲了實現這個算法,咱們須要一個數據結構,包含三個字段:
在React新的協調算法環境中,這個數據結構叫作Fiber。在內部,她表示了一個保持隊列工做的React節點,更多關於它的細節能夠看我下一篇文章。
如下實例圖示範了鏈表中連接對象組成結構,以及二者之間的關聯方式:
那讓咱們來定義咱們的定製的節點構造方法:
class Node {
constructor(instance) {
this.instance = instance;
this.child = null;
this.sibling = null;
this.return = null;
}
}
複製代碼
以及一個接受節點數組而後將它們鏈表起來的方法,咱們用這個方法將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;
}
複製代碼
這個方法從最後一個元素開始迭代一組節點,而後將它們連接成一個單鏈表。它返回列表的第一個兄弟節點,這裏有個關於它如何工做的簡單案例:
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 walk(o) {
let root = o;
let current = o;
while (true) {
// perform work for a node, retrieve & link the children
let child = doWork(current);
// if there's a child, set it as the current active node
if (child) {
current = child;
continue;
}
// if we've returned to the top, exit the function
if (current === root) {
return;
}
// keep going up until we find the sibling
while (!current.sibling) {
// if we've returned to the top, exit the function
if (!current.return || current.return === root) {
return;
}
// set the parent as the current active node
current = current.return;
}
// if found, set the sibling as the current active node
current = current.sibling;
}
}
複製代碼
雖然實現不是特別的難理解,但你可能須要稍微執行來領會它。思路是,咱們保持當前節點的引用,在沿着樹往下時,重複給其賦值,直到到達樹枝的末尾,而後,再使用return
指針返回給共同的父節點。
若是咱們如今檢查這個實現的調用棧時,能夠看到:
正如你看到的,這個棧不會隨着往下遍歷樹時增加,可是若是你在doWork
方法中加上dubugger,且打印節點的名稱,咱們就會看到以下狀況:
**這看起來想是一個瀏覽器的調用棧。**因此以這個算法,咱們用本身的實現有效地替換了瀏覽的調用棧實現。這正如Andrew描述的:
Fiber 是棧的從新實現,特別針對於React組件,你能夠認爲一個fiber就是一個虛擬的棧幀。
至此,咱們如今經過保持做爲頂層幀的節點的引用來控制着棧:
function walk(o) {
let root = o;
let current = o;
while (true) {
...
current = child;
...
current = current.return;
...
current = current.sibling;
}
}
複製代碼
咱們能夠在任意時刻中止遍歷,以後再繼續它。這確實咱們可以用在新requestIdleCallback
API而想要實現的狀況。
這裏的代碼實現了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);
}
}
}
複製代碼
正如你看到的,它很好地對應了咱們上面所說的算法。它在做爲頂層幀的nextUnitOfWork
變量中保持了當前fiber節點的引用。
這個算法能夠同步遍歷組件樹,且執行樹中每一個fiber節點的工做(nextUnitOfWork)。這個一般是由UI事件形成的所謂互動更新(click, input, etc)。或者它能夠在執行一個fiber節點的工做後,檢測是否還有剩餘時間,來異步遍歷組件樹。方法shouldYield
返回基於 deadlineDidExpire 和 deadline 變量的結果,這些變量會在React執行fiber節點工做時不斷地更新。
**peformUnitOfWork**
方法深度解析在這。