這是一篇講react Fiber演算法的文章,深入淺出,並且作者自己實現了Fiber的核心程式碼,可以很好的幫助我們理解fiber 原文連結
另外,建議讀這篇文章之前先看一下他的另外幾篇關於react的文章,本篇是建立在其之上的 DIY React
Didact Fiber: Incremental reconciliation
github repository updated demo
Why Fiber
本文並不會展示一個完整的React Fiber,如果你想了解更多,更多資料
當瀏覽器的主執行緒長時間忙於執行一些事情時,關鍵任務的執行可以能被推遲。
為了展示這個問題,我做了一個demo,為了使星球一直轉動,主執行緒需要每16ms被呼叫一次,因為animation是跑在主執行緒上的。如果主執行緒被其他事情佔用,假如佔用了200ms,你會發現animation會發生卡頓,星球停止執行,直到主執行緒空閒出來執行animation。
到底是什麼導致主執行緒如此繁忙導致不能空閒出幾微秒去保持動畫流暢和響應及時呢?
還記得以前實現的reconciliation code嗎?一旦開始,就無法停止。如果此時主執行緒需要做些別的事情,那就只能等待。並且因為使用了許多遞迴,導致很難暫停。這就是為什麼我們重寫程式碼,用迴圈代替遞迴。
Scheduling micro-tasks
我們需要把任務分成一個個子任務,在很短的時間裡執行結束掉。可以讓主執行緒先去做優先順序更高的任務,然後再回來做優先順序低的任務。
我們將會需要requestIdleCallback()函式的幫助。它在瀏覽器空閒時才執行callback函式,回撥函式中deadline
引數會告訴你還有多少空閒時間來執行程式碼,如果剩餘時間不夠,那麼你可以選擇不執行程式碼,保持了主執行緒不會被一直佔用。
const ENOUGH_TIME = 1; // milliseconds
let workQueue = [];
let nextUnitOfWork = null;
function schedule(task) {
workQueue.push(task);
requestIdleCallback(performWork);
}
function performWork(deadline) {
if (!nextUnitOfWork) {
nextUnitOfWork = workQueue.shift();
}
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork || workQueue.length > 0) {
requestIdleCallback(performWork);
}
}
複製程式碼
真正起作用的函式是performUnitOfWork。我們將會在其中寫reconciliation code。函式執行一次佔用很少的時間,並且返回下一次任務的資訊。
為了組織這些子任務,我們將會使用fibers
The fiber data structure
我們將會為每一個需要渲染的元件建立一個fiber。nextUnitOfWork
是對將要執行的下一個fiber的引用。performUnitOfWork
會對fiber進行diff,然後返回下一個fiber。這個將會在後面詳細解釋。
fiber是啥樣子的呢?
let fiber = {
tag: HOST_COMPONENT,
type: "div",
parent: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement("div"),
props: { children: [], className: "foo"},
partialState: null,
effectTag: PLACEMENT,
effects: []
};
複製程式碼
是一個物件啊,我們將會使用parent,child,sibling屬性去構建fiber樹來表示元件的結構樹。
stateNode
是對元件例項的引用。他可能是DOM元素或者使用者定義的類元件例項
舉個例子:
在上面例子中我們可以看到將支援三種不同的元件:
- b, p, i 代表著
host component
。我們將會用tag:HOST_COMPONENT來定義他。type屬性將會是字串。props是dom屬性和事件。 - Foo class component。它的tag:CLASS_COMPONENT, type指向使用者定義的類元件
- div代表著 host root。他類似於host component,stateNode也是DOM element.tag: HOST_ROOT.注意stateNode就是傳遞給render函式的引數。
另外一個重要屬性就是alternate
,我們需要它是因為大多數時間我們將會有兩個fiber tree。一個代表著已經渲染的dom, 我們成其為current tree 或者 old tree。另外一個是在更新(當呼叫setState或者render)時建立的,稱其為work-in-progress tree。
work-in-progress tree不會與old tree共享任何fiber。一旦我們完成work-in-progress tree的構建和dom的改變,work-in-progress tree就變成了old tree。
所以我們使用alternate屬性去連結old tree。fiber與其alternate有相同的tag,type,statenode。有時我們渲染新的元件,它可能沒有alternate屬性
然後,還有一個effects 列表和effectTag。當我們發現work-in-progress需要改變的DOM時,就將effectTag
設定為PLACEMENT
, UPDATE
, DELETION
。為了更容易知道總共有哪些需要fiber需要改變DOM,我們把所有的fiber放在effects列表裡。
可能這裡講了許多概念的東西,不要擔心,我們將會用行動來展示fiber。
Didact call hierarchy
為了對程式有整體的理解,我們先看一下結構示意圖
我們將會從render()
和setState()
開始,在commitAllWork()結束
Old code
我之前告訴你我們將重構大部分程式碼,但在這之前,我們先回顧一下不需要重構的程式碼
這裡我就不一一翻譯了,這些程式碼都是在文章開頭我提到的
- Element creation and JSX
- Instances, reconciliation and virtual DOM
- Components and state,這裡的 Class Component需要稍微改動一下
class Component {
constructor(props) {
this.props = props || {};
this.state = this.state || {};
}
setState(partialState) {
scheduleUpdate(this, partialState);
}
}
function createInstance(fiber) {
const instance = new fiber.type(fiber.props);
instance.__fiber = fiber;
return instance;
}
複製程式碼
render() & scheduleUpdate()
除了Component
, createElement
, 我們將會有兩個公共函式render()
, setState()
,我們已經看到setState()
僅僅呼叫了scheduleUpdate()
。
render()
和 scheduleUpdate()
非常類似,他們接收新的更新並且進入佇列。
/ Fiber tags
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";
// Global state
const updateQueue = [];
let nextUnitOfWork = null;
let pendingCommit = null;
function render(elements, containerDom) {
updateQueue.push({
from: HOST_ROOT,
dom: containerDom,
newProps: { children: elements }
});
requestIdleCallback(performWork);
}
function scheduleUpdate(instance, partialState) {
updateQueue.push({
from: CLASS_COMPONENT,
instance: instance,
partialState: partialState
});
requestIdleCallback(performWork);
}
複製程式碼
我們將會使用updateQueue
陣列來儲存等待的更新。每一次呼叫render
或者 scheduleUpdate
都會將資料儲存進updateQueue
。陣列裡每一個資料都不一樣,我們將會在resetNextUnitOfWork()
函式中使用。
在將資料push
儲存進佇列之後,我們將會非同步呼叫performWork()
。
performWork() && workLoop()
const ENOUGH_TIME = 1; // milliseconds
function performWork(deadline) {
workLoop(deadline);
if (nextUnitOfWork || updateQueue.length > 0) {
requestIdleCallback(performWork);
}
}
function workLoop(deadline) {
if (!nextUnitOfWork) {
resetNextUnitOfWork();
}
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (pendingCommit) {
commitAllWork(pendingCommit);
}
}
複製程式碼
這裡使用了我們之前看到的performUnitOfWork
模式。
workLoop()
中判斷deadline是不是有足夠的時間來執行程式碼,如果不夠,停止迴圈,回到performWork()
,並且nextUnitOfWork
還被保留為下次任務,在performWork()
中判斷是否還需要執行。
performUnitOfWork()
的作用是構建 work-in-progress tree和找到哪些需要操作DOM的改變。這種處理方式是遞增的,一次只處理一個fiber。
如果performUnitOfWork()
完成了本次更新的所有工作,則renturn值為null,並且呼叫commitAllWork
改變DOM。
至今為止,我們還沒有看到第一個nextUnitOfWork
是如何產生的
resetUnitOfWork()
函式取出updateQueue
第一項,將其轉換成nextUnitOfWork
.
function resetNextUnitOfWork() {
const update = updateQueue.shift();
if (!update) {
return;
}
// Copy the setState parameter from the update payload to the corresponding fiber
if (update.partialState) {
update.instance.__fiber.partialState = update.partialState;
}
const root =
update.from == HOST_ROOT
? update.dom._rootContainerFiber
: getRoot(update.instance.__fiber);
nextUnitOfWork = {
tag: HOST_ROOT,
stateNode: update.dom || root.stateNode,
props: update.newProps || root.props,
alternate: root
};
}
function getRoot(fiber) {
let node = fiber;
while (node.parent) {
node = node.parent;
}
return node;
}
複製程式碼
如果update包含partialState
, 就將其儲存的對應fiber上,在後面會賦值給元件例項,已供render使用。
然後,我們找到old fiber樹的根節點。如果update是first render呼叫的,root fiber將為null。如果是之後的render,root將等於_rootContainerFiber
。如果update是因為setState()
,則向上找到第一個沒有patient屬性的fiber。
然後我們將其賦值給nextUnitOfWork
,注意,這個fiber將會是work-in-progress的根元素。
如果沒有old root。stateNode將取render()中的引數。props將會是render()的另外一個引數。props中children是陣列。alternate
是 null。
如果有old root。stateNode是之前的root DOM node。props將會是newProps,如果其值不為null的話,否則就是原來的props。alternate
就是之前的old root。
我們現在已經有了work-in-progress的根元素,讓我們構造剩下的吧
performUnitOfWork()
function performUnitOfWork(wipFiber) {
beginWork(wipFiber);
if (wipFiber.child) {
return wipFiber.child;
}
// No child, we call completeWork until we find a sibling
let uow = wipFiber;
while (uow) {
completeWork(uow);
if (uow.sibling) {
// Sibling needs to beginWork
return uow.sibling;
}
uow = uow.parent;
}
}
複製程式碼
performUnitOfWork()
遍歷work-in-progress樹
beginWork()
的作用是建立子節點的fiber。並且將第一次子節點作為fiber的child屬性
如果當前fiber沒有子節點,我們就呼叫completeWork()
,並且返回sibling
作為下一個nextUnitOfWork
.
如果沒有sibling
,就繼續向上操作parent fiber。直到root。
總的來說,就是先處理葉子節點,然後是其兄弟節點,然後是雙親節點。從下往上遍歷。
beginWork(), updateHostComponent(), updateClassComponent()
unction beginWork(wipFiber) {
if (wipFiber.tag == CLASS_COMPONENT) {
updateClassComponent(wipFiber);
} else {
updateHostComponent(wipFiber);
}
}
function updateHostComponent(wipFiber) {
if (!wipFiber.stateNode) {
wipFiber.stateNode = createDomElement(wipFiber);
}
const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
function updateClassComponent(wipFiber) {
let instance = wipFiber.stateNode;
if (instance == null) {
// Call class constructor
instance = wipFiber.stateNode = createInstance(wipFiber);
} else if (wipFiber.props == instance.props && !wipFiber.partialState) {
// No need to render, clone children from last time
cloneChildFibers(wipFiber);
return;
}
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
const newChildElements = wipFiber.stateNode.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
複製程式碼
beginWork()
的作用有兩個
- 建立 stateNode
- 拿到component children,並且呼叫
reconcileChildrenArray()
因為對不同型別component的處理方式不同, 這裡分成了updateHostComponent
, updateClassComponent
兩個函式。
updateHostComponennt
處理了host component 和 root component。如果fiber上沒有DOM node則新建一個(僅僅是建立一個DOM節點,沒有子節點,也沒有插入到DOM中)。然後利用fiber props中的children去呼叫reconcileChildrenArray()
updateClassComponent
處理了使用者建立的class component。如果沒有例項則建立一個。並且更新了props和state,這樣render就是可以計算出新的children。
updateClassComponent
並不是每次都呼叫render函式。這有點類似於shouldCompnentUpdate
函式。如果不需要呼叫render,就複製子節點。
現在我們有了newChildElements
, 我們已經準備好去建立child fiber。
reconcileChildrenArray()
注意,這裡是核心。這裡建立了work-in-progress 樹和決定如何更新DOM
/ Effect tags
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;
function arrify(val) {
return val == null ? [] : Array.isArray(val) ? val : [val];
}
function reconcileChildrenArray(wipFiber, newChildElements) {
const elements = arrify(newChildElements);
let index = 0;
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber = null;
while (index < elements.length || oldFiber != null) {
const prevFiber = newFiber;
const element = index < elements.length && elements[index];
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
tag: oldFiber.tag,
stateNode: oldFiber.stateNode,
props: element.props,
parent: wipFiber,
alternate: oldFiber,
partialState: oldFiber.partialState,
effectTag: UPDATE
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
tag:
typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
effectTag: PLACEMENT
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = DELETION;
wipFiber.effects = wipFiber.effects || [];
wipFiber.effects.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index == 0) {
wipFiber.child = newFiber;
} else if (prevFiber && element) {
prevFiber.sibling = newFiber;
}
index++;
}
}
複製程式碼
首先我們確定newChildElements
是一個陣列(並不像之前的diff演算法,這次的演算法的children總是陣列,這意味著我們可以在render中返回陣列)
然後,開始將old fiber中的children與新的elements做對比。還記得嗎?fiber.alternate就是old fiber。new elements 來自於props.children
(function)和 render
(Class Component)。
reconciliation演算法首先diff wipFiber.alternate.child 和 elements[0],然後是 wipFiber.alternate.child.sibling 和 elements[1]。這樣一直遍歷到遍歷結束。
- 如果
oldFiber
和element
有相同的type。就通過old fiber建立新的。注意增加了UPDATE effectTag
- 如果這兩者有不同的type或者沒有對應的oldFiber(因為我們新新增了子節點),就建立新的fiber。注意新fiber不會有
alternate
屬性和stateNode(stateNode就會在beginWork()
中建立)。還增加了PLACEMENT effectTag
。 - 如果這兩者有不同的type或者沒有對應的
element
(因為我們刪除了一些子節點)。我們標記old fiberDELETION
。
cloneChildFibers()
updateClassComponent
中有一個特殊情況,就是不需要render,而是直接複製fiber。
function cloneChildFibers(parentFiber) {
const oldFiber = parentFiber.alternate;
if (!oldFiber.child) {
return;
}
let oldChild = oldFiber.child;
let prevChild = null;
while (oldChild) {
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber
};
if (prevChild) {
prevChild.sibling = newChild;
} else {
parentFiber.child = newChild;
}
prevChild = newChild;
oldChild = oldChild.sibling;
}
}
複製程式碼
cloneChildFibers()
拷貝了old fiber的所有的子fiber。我們不需要增加effectTag
,因為我們確定不需要改變什麼。
completeWork()
performUnitOfWork
, 當wipFiber 沒有新的子節點,或者我們已經處理了所有的子節點時,我們呼叫completeWork
.
function completeWork(fiber) {
if (fiber.tag == CLASS_COMPONENT) {
fiber.stateNode.__fiber = fiber;
}
if (fiber.parent) {
const childEffects = fiber.effects || [];
const thisEffect = fiber.effectTag != null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
} else {
pendingCommit = fiber;
}
}
複製程式碼
在 completeWork
中,我們新建了effects列表。其中包含了work-in-progress中所有包含effecTag
。方便後面處理。最後我們將pendingCommit指向了root fiber。並且在workLoop
中使用。
commitAllWork & commitWork
這是最後一件我們需要做的事情,改變DOM。
function commitAllWork(fiber) {
fiber.effects.forEach(f => {
commitWork(f);
});
fiber.stateNode._rootContainerFiber = fiber;
nextUnitOfWork = null;
pendingCommit = null;
}
function commitWork(fiber) {
if (fiber.tag == HOST_ROOT) {
return;
}
let domParentFiber = fiber.parent;
while (domParentFiber.tag == CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.stateNode;
if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
domParent.appendChild(fiber.stateNode);
} else if (fiber.effectTag == UPDATE) {
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag == DELETION) {
commitDeletion(fiber, domParent);
}
}
function commitDeletion(fiber, domParent) {
let node = fiber;
while (true) {
if (node.tag == CLASS_COMPONENT) {
node = node.child;
continue;
}
domParent.removeChild(node.stateNode);
while (node != fiber && !node.sibling) {
node = node.parent;
}
if (node == fiber) {
return;
}
node = node.sibling;
}
}
複製程式碼
commitAllWork
首先遍歷了所有的根root effects。
- PLACEMENT。將dom插入到父節點上
- UPDATE。將新舊props交給
updateDomProperties()
處理。 - DELETION。如果是Host component。用removeChild()刪除就好。如果是class Component,那就要刪除fiber subTree下面的所有host Component。
一旦我們完成了所有的effects,就重置nextUnitOfWork
和pendingCommit
。work-in-progress tree就變成了old tree。並複製給_rootContainerFiber
。
這樣我們完成了更新,並且做好了等待下一次更新的準備。
更多文章請檢視我的主頁或者blog