react fiber介紹(譯)
這是一篇講react Fiber演算法的文章,深入淺出,並且作者自己實現了Fiber的核心程式碼,可以很好的幫助我們理解fiber原文連結
另外,建議讀這篇文章之前先看一下他的另外幾篇關於react的文章,本篇是建立在其之上的DIY React
Didact Fiber: Incremental reconciliation
ofollow,noindex">github repository updated demo
Why Fiber
本文並不會展示一個完整的React Fiber,如果你想了解更多, 更多資料
當瀏覽器的主執行緒長時間忙於執行一些事情時,關鍵任務的執行可以能被推遲。
為了展示這個問題,我做了一個demo,為了使星球一直轉動,主執行緒需要每16ms被呼叫一次,因為animation是跑在主執行緒上的。如果主執行緒被其他事情佔用,假如佔用了200ms,你會發現animation會發生卡頓,星球停止執行,直到主執行緒空閒出來執行animation。
到底是什麼導致主執行緒如此繁忙導致不能空閒出幾微秒去保持動畫流暢和響應及時呢?
還記得以前實現的reconciliation code嗎?一旦開始,就無法停止。如果此時主執行緒需要做些別的事情,那就只能等待。並且因為使用了許多遞迴,導致很難暫停。這就是為什麼我們重寫程式碼,用迴圈代替遞迴。
Scheduling micro-tasks
我們需要把任務分成一個個子任務,在很短的時間裡執行結束掉。可以讓主執行緒先去做優先順序更高的任務,然後再回來做優先順序低的任務。
我們將會需要 API%2FWindow%2FrequestIdleCallback" rel="nofollow,noindex">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元素或者使用者定義的類元件例項
舉個例子:

在上面例子中我們可以看到將支援三種不同的元件:
host component
另外一個重要屬性就是 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()
的作用有兩個
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。
updateDomProperties()
一旦我們完成了所有的effects,就重置 nextUnitOfWork
和 pendingCommit
。work-in-progress tree就變成了old tree。並複製給 _rootContainerFiber
。 這樣我們完成了更新,並且做好了等待下一次更新的準備。
更多文章請檢視我的主頁或者blog