📝

React Fiberのアルゴリズムを実装してみた

2021/04/13に公開

いまさらながらReact Fiberのアルゴリズムを実装してみました。

https://github.com/okmttdhr/my-own-react

300行くらいですが、プログラムの名前や責務など、本物のReactをできるかぎり忠実に再現するようにしています。実装は以下にポイントを絞りました。

  • Fiberのデータ構造とトラバースアルゴリズム
  • 中断可能な「Unit of work」
  • RenderフェーズとCommitフェーズ

アーキテクチャは以下のような感じ

architecture diagram

コード上にReactのソースコードへのリンクもコメントしているので、見比べてみるとより理解が深まると思います(例えば、今回はrequestIdleCallbackで擬似的にスケジューリングを実現していますが、Reactでは独自にSchedulerをつくっています)。

Fiberのアーキテクチャに関しては既にたくさんの情報があるため、この記事ではいくつか登場人物を紹介するまでとします。

performUnitOfWork

Fiberをトラバースして次のUnit of workを決める

// https://github.com/okmttdhr/react/blob/master/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3206
function beginWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }

  if (fiber.child) {
    return fiber.child
  }

  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1574
function performUnitOfWork(fiber) {
  nextUnitOfWork = beginWork(fiber)
  if (!nextUnitOfWork) {
    commitRoot()
    completeUnitOfWork()
  }
}

workLoop

仕事がなくなるまでループする
ブラウザが忙しくなったらループを中断して終わってから戻ってくる

// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L1567
function workLoop(deadline) {
  let shouldYield = false
  while (workInProgressRoot && !shouldYield) {
    performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}

reconcileChildren

差分検知とfiberの更新

// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L255
function reconcileChildren(workInProgressFiber, elements) {
  let index = 0
  let oldFiber =
    workInProgressFiber.alternate && workInProgressFiber.alternate.child
  let prevSibling = null

  while (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = null

    const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: workInProgressFiber,
        alternate: oldFiber,
        flag: UPDATE,
      }
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: workInProgressFiber,
        alternate: null,
        flag: PLACEMENT,
      }
    }
    if (oldFiber && !sameType) {
      oldFiber.flag = DELETION
      deletions.push(oldFiber)
    }

    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }

    if (index === 0) {
      workInProgressFiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}

commitWork

DOMの反映(Commitフェーズ)

// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.new.js#L1814
function commitWork(fiber) {
  if (!fiber) {
    return
  }

  let parentFiber = fiber.parent
  while (!parentFiber.dom) {
    parentFiber = parentFiber.parent
  }
  const parentDom = parentFiber.dom

  if (
    fiber.flag === PLACEMENT &&
    fiber.dom != null
  ) {
    commitPlacement(fiber, parentDom)
  } else if (
    fiber.flag === UPDATE &&
    fiber.dom != null
  ) {
    commitUpdate(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.flag === DELETION) {
    commitDeletion(fiber, parentDom)
  }

  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

// https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1693
function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(workInProgressRoot.child)
}

突っ込みやPRなど歓迎です。

参考

Discussion