at backyard

Color my life with the chaos of trouble.

Reactを自作するための学習メモ #3

Reactを自作するための学習の記録の続き

この続き

shinshin86.hateblo.jp

参照ドキュメント

現在ここに記述しているコードは、下記のドキュメントのほぼ写経となっています。

pomb.us

zenn.dev

おさらい

ここまでの実装は下記。

なお、createElementrender は参照ドキュメントに倣いDidact.jsというファイルに分割した。

  • Didact.js(reactが本来請け負う実装)
  • index.js(実際の処理)
// Didact.js
const createElement = (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child =>
          typeof child === "object"
            ? child
            : createTextElement(child)
        ),
      }
    };
  }
  
  const createTextElement = text => {
    return {
      type: "TEXT_ELEMENT",
      props: {
        nodeValue: text,
        children: [],
      },
    }
  }
  
  const render = (element, container) => {
    const dom = element.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
  
    const isProperty = key => key !== "children"
    Object.keys(element.props)
      .filter(isProperty)
      .forEach(name => {
        dom[name] = element.props[name]
      })
  
    element.props.children.forEach(child =>
      render(child, dom)
    )
  
    container.appendChild(dom)
  }

  export {
      createElement,
      render
  }
// index.js
import { createElement, render } from "./Didact";

/** @jsxRuntime classic */
/** @jsx createElement */
const element = (
  <div id="foo">
    <h1>Hello</h1>
    <h2>World</h2>
    <div>
      <p>aaaaa</p>
      <p>bbbbb</p>
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
)

const container = document.getElementById("root");
render(element, container);

現在こんな感じになっている。

@jsxRuntime classic の箇所については下記に別途書いたので、気になる方は見てみてください。

shinshin86.hateblo.jp

再帰呼び出し部分の修正(render関数)

次は再帰呼び出し部分の修正を行っていく。

現在の実装では、要素ツリーをレンダリングし終えるまで停止しない実装となっている。

const render = (element, container) => {
  ・
  ・
  ・
  // ここの部分
  element.props.children.forEach(child =>
      render(child, dom)
    )
  ・
  ・
  ・
}

そのため、ここの処理を改修していく。

各ユニットが終了した後、他に実行させる必要があることがあれば、ブラウザにレンダリングを中断させるなど、柔軟な処理が行えるようにしていく。

ここで登場するのが requestIdleCallback というWeb API
メインスレッドがアイドル状態のときにブラウザがコールバックを実行してくれるらしい。

また、requestIdleCallback はtimeoutのパラメーターも扱えるようで、これを使用して、ブラウザが再び制御する必要があるまでの時間を制御できるようだ。

developer.mozilla.org

なお、現在のReactの実装ではこの関数は利用しておらず、別のスケジューラパッケージを利用しているとのことだが、概念としては同じようだ。

Fiberについて

また、ここではファイバーツリーというデータ構造も出てくる。

詳細は下記に記載されている。

github.com

また、下記の日本語の記事は非常にわかりやすかった。

html5experts.jp

このFiberというのは、Reactの更新処理に優先度が付けられるようにするために設定された作業の構成単位のことらしい。
といっても、うまく理解できないので、もう少し細かく見ていく。

Fiberはelementごとに一本のファイバーがあり、これら1本1本のファイバーが作業単位になるらしい。
作業単位、という言葉が出てきたが、具体的には

  • 要素をDOMに追加する
  • 子要素のためのファイバーを新たに作成する
  • 次の作業単位(ファイバーのこと)を選択する

ということを行うようだ。

ちなみにこのファイバーを用いたデータ構造をファイバーツリーというらしい。

このファイバーツリー自体の目的は、次に作業を行うべき要素を簡単に見つけられるようにすることらしい。
またこのことを実現するためにはファイバーには

  • 最初の子(child)
  • 次の兄弟(sibling) → sibling という単語にあまり馴染みがなかったが、兄弟のことだ
  • 親(parent)

という3つのリンクを持つようだ。

例えば下記の構造を処理していきたいとする。
(この構造は参照しているページの構造を引用している)

<div>
  <h1>
    <p />
    <a />
  </h1>
  <h2 />
</div>

この場合、Fiber Treeは下記のようになるようだ。

f:id:shinshin86:20210708213603p:plain
Fiber Tree

また、ファイバーに 兄弟 もいない場合がある。
このようなケースでは おじ 、つまり親の兄弟に移動するようだ。

例えば、上の例で <a> が子も兄弟もいない場合は、 <a> → <h2> となることになる。

また、親に兄弟がいない場合は、親の兄弟が見つかるまでさかのぼって調べていくとのこと。

それでも見つからなければrootまでたどり着くようだ。

ここまでのソースコードを下に貼る。
(といっても、写経したコードです)

// Didact.js
const createElement = (type, props, ...children) => {
    return {
        type,
        props: {
            ...props,
            children: children.map(child =>
                typeof child === "object"
                    ? child
                    : createTextElement(child)
            ),
        }
    };
}

const createTextElement = text => {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: [],
        },
    }
}

const createDom = fiber => {
    const dom = fiber.type === "TEXT_ELEMENT"
        ? document.createTextNode("")
        : document.createElement(fiber.type)

    const isProperty = key => key !== "children"
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {
            dom[name] = fiber.props[name]
        })

    return dom
}

const render = (element, container) => {
    nextUnitOfWork = {
        dom: container,
        props: {
            children: [element],
        },
    }
}

let nextUnitOfWork = null;

const workLoop = deadline => {
    let shouldYield = false

    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(
            nextUnitOfWork
        )

        shouldYield = deadline.timeRemaining() < 1
    }
    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

const performUnitOfWork = fiber => {
    console.log(fiber) // fiberの内容を開発者ツールのconsole上でチェックするためにログを出す

    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    if (fiber.parent) {
        fiber.parent.dom.appendChild(fiber.dom)
    }

    const elements = fiber.props.children
    let index = 0
    let prevSibling = null

    while (index < elements.length) {
        const element = elements[index]

        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

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

        prevSibling = newFiber
        index++
    }

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

    let nextFiber = fiber

    while (nextFiber) {
        if (nextFiber.sibling) {
            return nextFiber.sibling
        }

        nextFiber = nextFiber.parent
    }
}

export {
    createElement,
    render
}

途中console.logをはさみ、fiberの中身を確認している

次はindex.js

import { createElement, render } from "./Didact";

/** @jsxRuntime classic */
/** @jsx createElement */
const element = (
  <div id="foo">
    <h1>Hello</h1>
    <h2>World</h2>
    <div>
      <p>aaaaa</p>
      <p>bbbbb</p>
      <ul>
        <li>list 1</li>
        <li>list 2</li>
        <li>list 3</li>
      </ul>
    </div>
  </div>
)

const container = document.getElementById("root");
render(element, container);

Fiberの概要を理解してからであれば、ソースコードで何をしたいかも見えてくる。

が、正直これは写経しているからというのもあるが、なるほどーとは思うが、ここらへんは1から自分で実装しないと、意図やこのようにすることで達成できるものなどがうまく想像できない。

一旦ここについてはこのまま進めて、また後ほど紐解いていってみようと思う。