React的新引擎—React Fiber是什麼?

這是一篇講react Fiber算法的文章,深刻淺出,而且做者本身實現了Fiber的核心代碼,能夠很好的幫助咱們理解fiber 原文連接html

另外,建議讀這篇文章以前先看一下他的另外幾篇關於react的文章,本篇是創建在其之上的 DIY Reactnode

Didact Fiber: Incremental reconciliation

github repository updated demoreact

Why Fiber

本文並不會展現一個完整的React Fiber,若是你想了解更多,更多資料git

當瀏覽器的主線程長時間忙於運行一些事情時,關鍵任務的執行能夠能被推遲。github

爲了展現這個問題,我作了一個demo,爲了使星球一直轉動,主線程須要每16ms被調用一次,由於animation是跑在主線程上的。若是主線程被其餘事情佔用,假如佔用了200ms,你會發現animation會發生卡頓,星球中止運行,直到主線程空閒出來運行animation。算法

究竟是什麼致使主線程如此繁忙致使不能空閒出幾微秒去保持動畫流暢和響應及時呢?數組

還記得之前實現的reconciliation code嗎?一旦開始,就沒法中止。若是此時主線程須要作些別的事情,那就只能等待。而且由於使用了許多遞歸,致使很難暫停。這就是爲何咱們重寫代碼,用循環代替遞歸。瀏覽器

Scheduling micro-tasks

咱們須要把任務分紅一個個子任務,在很短的時間裏運行結束掉。可讓主線程先去作優先級更高的任務,而後再回來作優先級低的任務。app

咱們將會須要requestIdleCallback()函數的幫助。它在瀏覽器空閒時才執行callback函數,回調函數中deadline參數會告訴你還有多少空閒時間來運行代碼,若是剩餘時間不夠,那麼你能夠選擇不執行代碼,保持了主線程不會被一直佔用。dom

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

我以前告訴你咱們將重構大部分代碼,但在這以前,咱們先回顧一下不須要重構的代碼

這裏我就不一一翻譯了,這些代碼都是在文章開頭我提到的

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的處理方式不一樣, 這裏分紅了updateHostComponentupdateClassComponent兩個函數。

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]。這樣一直遍歷到遍歷結束。

  • 若是oldFiberelement有相同的type。就經過old fiber建立新的。注意增長了UPDATE effectTag
  • 若是這二者有不一樣的type或者沒有對應的oldFiber(由於咱們新添加了子節點),就建立新的fiber。注意新fiber不會有alternate屬性和stateNode(stateNode就會在beginWork()中建立)。還增長了PLACEMENT effectTag
  • 若是這二者有不一樣的type或者沒有對應的element(由於咱們刪除了一些子節點)。咱們標記old fiber DELETION

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,就重置nextUnitOfWorkpendingCommit。work-in-progress tree就變成了old tree。並複製給_rootContainerFiber。 這樣咱們完成了更新,而且作好了等待下一次更新的準備。

更多文章請查看個人主頁或者blog

相關文章
相關標籤/搜索