at backyard

Color my life with the chaos of trouble.

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

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

この続き

shinshin86.hateblo.jp

参照ドキュメント

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

pomb.us

zenn.dev

関数コンポーネントに対応する

現在の実装では関数コンポーネントに対応していない。

例えば下記のようなコードを書いた場合、ブラウザアクセス時にエラーが発生する。

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

/** @jsxRuntime classic */
/** @jsx createElement */
const App = props => (
  <div>
    <h1>Hi {props.name}</h1>
    <ul>
      <li>aaa</li>
      <li>bbb</li>
      <li>ccc</li>
    </ul>
  </div>
) 

const element = <App name="foo" />

const container = document.getElementById("root")

render(element, container)

エラーの内容はこんなやつ

Uncaught DOMException: Failed to execute 'createElement' on 'Document': The tag name provided ('props => Object(_Didact__WEBPACK_IMPORTED_MODULE_0__["createElement"])("div", {
  __self: undefined,
・
・
・

関数コンポーネントは、クラスコンポーネントとは下記のような異なる点があるため、現在の実装では正常に処理が行えない。

  • 関数コンポーネントからのファイバーにはDOMノードが存在しない
  • 子要素は、propsから直接取得するのではなく、関数を実行することから取得できる

そのため、 performUnitOfWork関数でファイバーの型が関数かどうかを確認して、処理を振り分けるようにする必要がある。

以前と同じ処理は、updateHostComponentで実行するようにし、関数コンポーネントの場合は、 updateFunctionComponent で処理を実行するようにコードを追加していくことにする。
(参照元のドキュメントに倣っています)

渡されたコンポーネントが関数だった場合、 updateFunctionComponent では関数を実行して子を取得するようにしていく。 子要素を取得できれば、差分検出の処理などは同じように機能するため、以後の処理に変更を加える必要はない。

ただし、commitWork関数については変更の必要がある。
関数コンポーネントの場合、DOMノードがないfiberであるため、DOMノードの親を見つける必要がある。

よって、下記のようなコードとなる。 (説明放棄)

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 isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
const isEvent = key => key.startsWith("on")
const isProperty = key => key !== "children" && !isEvent(key)

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

    updateDom(dom, {}, fiber.props)

    return dom
}

const updateDom = (dom, prevProps, nextProps) => {
    // Remove or modify event listeners
    Object.keys(prevProps)
        .filter(isEvent)
        .filter(
            key =>
                !(key in nextProps) ||
                isNew(prevProps, nextProps)(key)
        )
        .forEach(name => {
            const eventType = name
                .toLowerCase()
                .substring(2)
            dom.removeEventListener(
                eventType,
                prevProps[name]
            )
        })

    // Delete old properties
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })

    // Set a new or changed property
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })

    // Add a new event listener
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            const eventType = name
                .toLowerCase()
                .substring(2)
            dom.addEventListener(
                eventType,
                nextProps[name]
            )
        })
}

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

    deletions = []

    nextUnitOfWork = wipRoot
}

let wipRoot = null
let currentRoot = null
let nextUnitOfWork = null;
let deletions = null

const commitRoot = () => {
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
}

const commitWork = fiber => {
    if (!fiber) {
        return
    }

    // DOMノードを持つファイバーが見つかるまでファイバーツリーを上に移動
    let domParentFiber = fiber.parent
    while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent }
    const domParent = domParentFiber.dom


    if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
        domParent.appendChild(fiber.dom)
    } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props)
    } else if (fiber.effectTag === "DELETION") {
        commitDeletion(fiber, domParent) // ノードを削除するときは、DOMノードを持つ子が見つかるまで探索を続行
    }

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

const commitDeletion = (fiber, domParent) => {
    if (fiber.dom) {
        domParent.removeChild(fiber.dom)
    } else {
        commitDeletion(fiber.child, domParent)
    }
}

const workLoop = deadline => {
    let shouldYield = false

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

        shouldYield = deadline.timeRemaining() < 1
    }

    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }

    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

const performUnitOfWork = 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
    }
}

const updateFunctionComponent = fiber => {
    const children = [fiber.type(fiber.props)]

    reconcileChildren(fiber, children)
}

const updateHostComponent = fiber => {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    reconcileChildren(fiber, fiber.props.children)
}

const reconcileChildren = (wipFiber, elements) => {
    let index = 0
    let oldFiber = wipFiber.alternate && wipFiber.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 the old fiber and the new element are of the same type, keep the DOM node and update it with the new props
        if (sameType) {
            // Update process for nodes
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }

        // If the type is different and there is a new element, a new DOM node needs to be created
        if (element && !sameType) {
            // Adding a node
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: "PLACEMENT",
            }
        }

        // If different types and old fibers are present, delete the old node
        if (oldFiber && !sameType) {
            // Deletion process of old fiber nodes
            oldFiber.effectTag = "DELETION"
            deletions.push(oldFiber)
        }

        if (oldFiber) {
            oldFiber = oldFiber.sibling
        }

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

        prevSibling = newFiber
        index++
    }
}

export {
    createElement,
    render
}

useState(hooks)に対応させる

関数コンポーネントの対応の次は useStateにも対応させる。

クリックするたびに、stateのカウンター値が1つ増えるような、サンプルとしてよくある例にコードを変更する。

index.jsを下記のように変更して、これが動くようにDidact.jsを修正していく。

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

/** @jsxRuntime classic */
/** @jsx createElement */
const App = () => {
  const [ count, setCount ] = useState(1)

  return (
    <h1 onClick={() => setCount(count => count + 1)}>
      Count: {count}
    </h1>
  )
}

const element = <App name="foo" />

const container = document.getElementById("root")

render(element, container)

関数コンポーネントuseState を呼び出すとき、既に古いフックがあるかどうかを確認し、 hookIndexを用いてwipFiber.alternate をチェックするようにする。
もし古いフックがある場合は、stateを古いフックから新しいフックにコピーする。そうでない場合は初期化。

新しいフックをファイバーに追加する際は、 hookIndexを1つインクリメントしてstateを返すようにする。

また useState はstateを更新する関数を返す必要があるため(ここでは setCount 関数)、アクションを受け取る setState関数を定義する。

そのアクションをフックに追加したキューにpushする。

そして、作業のループが新しいレンダリングフェーズを開始できるように、新しい wipRootを次の作業単位として設定する。

ここらへんのコードは下記のように実装。
(何度もしつこいですが、参照元の写経です)

const useState = initial => {
    const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]
    const hook = {
        state: oldHook ? oldHook.state : initial,
        queue: []
    }

    const actions = oldHook ? oldHook.queue : [];
    actions.forEach((action) => {
        hook.state = action(hook.state);
    });

    const setState = action => {
        hook.queue.push(action)
        wipRoot = {
            dom: currentRoot.dom,
            props: currentRoot.props,
            alternate: currentRoot,
          }
        nextUnitOfWork = wipRoot
        deletions = []
    }


    wipFiber.hook.push(hook)
    hookIndex++
    return [hook.state, setState]
}

・
・
・

export {
    createElement,
    render,
    useState
}

最後に

ひとまずドキュメントを写経しながらという形でReact自作に向けた学習を進めてきた。

が、一旦この学習はここで中断する。

他にやりたいタスクが残っているのと、Reactを1から自作するというのはなかなか時間がかかりそうだということがわかり、それに対して今は時間を確保できなそうだからだ。

あと、Fiber周りの仕組みなどがまだまだ理解が追いついていないので、ここについてはもう少し復習しても良さそうに思える。

実際のReactのコードも参照しつつ進めていたが、React本家のコードを細かく追っていくのもなかなかに骨が折れる作業だということも分かった。

ただ、Reactがどういう仕組で実際にHTMLを生成していくのか、差分検出はどのような考え方で実行されているのか?など今までほぼほぼブラックボックスとなっていた中身が想像できるようになったのは、今後もReactを触っていく上では大きいことのように思う。

また時間を見つけてReact自作に向けた学習は進めていく予定。

なお、ここまでのコードは下記に載せている。
後々またこのリポジトリにコードを追加していくかもしれないので、コミットログと併せてご参照ください。
(この時点での最新のコードはコミットハッシュが 23bc60265d2a1813f1751708536d4b4d9298eecc となっています)

github.com

MDN Web Docsへのコントリビュート手順(備忘録)

f:id:shinshin86:20210715224844p:plain

これはMDN Web Docsへのコントリビュートを実際に行った際の手順などをメモした備忘録となる。

目次

アカウントを作成(これは不要かもしれない)

MDN Web Docsにアクセスすると、画面右上にあるSigninからアカウントを作成できる。
これが必須なのかはわからないが、とりあえずアカウントは作成してみた。
GitHub連携かGoogle連携か選べたので、GitHub連携で作成してみた。

ただ、その後の作業手順を考えると、別にコントリビュートをするのにアカウントは作らなくても良かったのではないかとも思ったりした。
(コントリビューション自体はGitHubアカウントがあればできそうだったので)

コントリビューションに関するMDNのドキュメント

コントリビューションに関するMDNのドキュメントは下記に記載されている。

まずはこちらを読んだ。

content/README.md at main · mdn/content · GitHub

MDNへコントリビューションする際はGitHubアカウントが必須

修正作業はGitHub経由で行われるのでアカウントを持っていない方はアカウントを作成するところから始める必要がある。

MDN の構成について

MDNのドキュメントとGitHubで格納されている実際のファイルの関係については下記に記載がある。

content/README.md at main · mdn/content · GitHub

英語以外のドキュメントにコントリビューションしたいとき

ちなみに上記リポジトリは英語のドキュメントのみを扱っている。

他の言語のドキュメントについてコントリビューションを行いたい場合は、下記のリポジトリを見る必要がある。

GitHub - mdn/translated-content: All translated MDN content in raw form

なお、現在は下記の言語しかコントリビューションを受け付けていないようだ。
これはアクティブなメンテナンスチームの言語のみを表しているとのこと。

  • fr
  • ja
  • ko
  • pt-BR
  • ru
  • zh (zh-CN and zh-TW)

修正対象のファイルの探し方

上の MDN の構成について でも書いたが、URLを見ながら実際に修正対象となるファイルを探していくことになる。

MDN Web Docsへコントリビューションする際に事前に知っておくべき情報は、これで以上となる。

ここからは実際にリポジトリで修正作業を行った際、修正内容をローカルで確認する方法について記載していく。

ローカルサーバで修正したドキュメント内容を確認する方法

1. まずは下記のリポジトリをどちらもcloneする

下記がMDN Web Docsの本体(英語のみ)

github.com

下記がMDN Web Docsの多言語に関するファイルが格納されている。

github.com

2. content repoのセットアップ

cloneした2つのリポジトリのうち、contentのルートで下記のコマンドを打って依存パッケージをインストールする
(ちなみに Node.jsのインストールと、 yarnのインストールは事前に済ませておくこと)

yarn install

3. 環境変数を追加

cloneしていたもう一つのリポジトリ translated-contentfiles のパスを CONTENT_TRANSLATED_ROOT にセットした状態で yarn startを行う

実際に打つ際のコマンドは下記。

CONTENT_TRANSLATED_ROOT=/path/to/translated-content/files yarn start

もしくは export環境変数を設定してからでも良い。

export CONTENT_TRANSLATED_ROOT=/path/to/translated-content/files
yarn start

すると、ローカルサーバが立ち上がるので、 localhost:5000 にアクセスして確認が可能。
(アクセスした画面から指定のページに遷移し、 Change Language から指定の言語に変更できる)

環境変数追加の別の方法

上のやり方以外にも .env ファイルを用いて設定する方法もある。 こちらであれば以降はyarn startだけでOKとなる。

echo CONTENT_TRANSLATED_ROOT=/path/to/translated-content/files >> .env

# .envに設定情報は記憶されるので、あとは "yarn start"でOK
yarn start

ローカルでの確認方法については以上である。

実際にコントリビューションしてみた

今回修正しようと思ったファイルはこちらである。

https://developer.mozilla.org/ja/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_todo_list_beginning

日本語の誤字を見つけたので、その修正PRを作成することにした。

まずは上にも書いたtranslated-contentリポジトリを自身のGitHubアカウントにforkする

github.com

次は人によるかもしれないが、私はローカルに一度cloneしてから修正PRを作成、

上で書いたローカルサーバを立ち上げる方法で修正内容を確認したりしながら作業を実施。

実際に作成したPRが下記となる。

github.com

修正で凡ミスをしてしまっており恥ずかしい。 またPRを見てもらうと分かる通り、日本人の方に対応していただき、コミュニケーションは日本語で行うことができた。
(まだ一度PRを作っただけのため、毎回、日本語で行えるかどうかまでは分からない)

またPRのやり取りを見てもらうと分かる通り、実は日本語のドキュメントには修正必要な箇所や、不要なfontタグがソース内に散りばめられており(自動生成した際の痕跡とか?)、修正が必要な箇所は結構ありそうでした。

なお、日本語のドキュメントを修正する際は下記のガイドラインにも目を通しておくと良さそうだ。

github.com

というわけで、もしこの記事がMDNへコントリビューションしようか迷っている方の背中を押すきっかけになれば幸いです。

ちなみにMDNのPRを作成すると、最初に push した分の修正に対してPreview URLが発行されるので、そちらで一度修正内容を確認してみるのも良さそうです。
(自分はそこで確認するのも漏れていたので、これは自分に対する戒めでもあります。)

Markdownを書いてサクッとHTMLを生成したいときに便利なツールを見つけた

md2htmlというツールがサクッとMarkdownからHTMLを生成できて便利

普段MakrdownからHTMLを生成するときは、JSなどで簡単なスクリプトを書いて変換をしていたが、そういうことをするのも面倒に思えたある日、下記のようなツールを見つけた。

md2html

使い方は簡単で、markdownに何かを書けば、右側のウィンドウにHTMLが生成される。

後はそれをコピペするなり何なりして利用すればOKというもの。

こういうサクッと使えるツールを知っておくといざというときに便利だなーと思ったので、こちらのブログにも書き残しておく。

作者の方のブログはこちら

blog.ikappio.com

便利なツールを公開してくれていて、非常にありがたい

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

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

この続き

shinshin86.hateblo.jp

参照ドキュメント

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

pomb.us

zenn.dev

Render Phase と Commit Phase のおはなし

Reactはページをレンダリングするとき、2段階に分けて処理を行う。

Render Phase

↓

Commit Phase

という順番で処理が行われるそうで、最初のRender PhaseではReactは仮想DOMを作成する。

この段階で仮想DOMが作成されるので、まだ画面上に変化はないが、実際にどのような画面が作成されるかが決定する。

Reactは最上位のコンポーネントでRenderを呼び出し、さらにその中のコンポーネント内のRenderを見に行く、といった形で子要素ごとにRenderを呼び出しページ全体がどの様になっているかが分かるまで再帰的にこの処理が行われる。

その次のフェーズとして、Commit Phaseというのがある。

仮想DOMの内容が判明したら、その内容と一致するように実際のDOMを更新していくフェーズというのが、このCommit Phaseとなる。

  • Render Phaseから取得した最新のDOMを、
  • 最後に取得した画面上での最新のDOMと比較し、
  • ページを最小限の処理で、最新の状態に更新するように計算していく

ここまでCommit Phaseとなる。

Render Phase と Commit Phase の実装を加える

実はここまでの実装では、ツリー全体のレンダリングが完了する前に、ブラウザが作業を中断すると不完全なUIが表示されるような状態となってしまっている。

これらを解消すために、このRender Phase と Commit Phase の実装を加えていく。

まずはDOMノードを追跡する箇所を削除する。

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

代わりに #3 に登場したファイバツリーのルートを追跡していくようにする。
そこで wipRoot という変数を用いることにする。

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

    nextUnitOfWork = wipRoot
}

let wipRoot = null

そして次の作業単位( nextUnitOfWork )がnullとなった場合、ファイバーツリー全体をDOMにコミットする処理を行う。 (作業単位がnullになったということは、=すべての作業が終了したことを意味する)

workLoop 関数を以下のように書き換え、

const workLoop = deadline => {
    let shouldYield = false

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

        shouldYield = deadline.timeRemaining() < 1
    }

    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }

    requestIdleCallback(workLoop)
}

commitRootcommitWork 関数をそれぞれ加える。

const commitRoot = () => {
    commitWork(wipRoot.child)
    wipRoot = null
}

const commitWork = fiber => {
    console.log(fiber); // ログをコンソールに出す

    if (!fiber) {
        return
    }

    const domParent = fiber.parent.dom
    domParent.appendChild(fiber.dom)
    commitWork(fiber.child)
    commitWork(fiber.sibling)
}

ちなみに上でコンソールログを出しているが、こんな感じのものが処理が行われるたびに表示される。

f:id:shinshin86:20210709220648p:plain
commitWork関数内のログ

差分検出を実装する

上の処理まででDOMの追加を行うようになったが、ノードの更新や削除を実装する必要がある。

render関数で受け取ったelementと、DOMにコミット済みの最新のファイバーツリーとを比較する処理を追加していく。

が、比較するには、最後にそのDOMにコミットした際のファイバーツリーの参照を覚えておく必要がある。
そこで、これを currentRoot という変数に持たせるようにする。

さらにすべてのファイバーに alternate プロパティを追加し、ここに前回のDOMコミット時のファイバーツリー参照をもたせる。

const commitRoot = () => {
    commitWork(wipRoot.child)
    currentRoot = wipRoot // ファイバーツリーの参照を渡しておく
    wipRoot = null
}
const render = (element, container) => {
    wipRoot = {
        dom: container,
        props: {
            children: [element]
        },
        alternate: currentRoot, // ここに前回のDOMコミット時のファイバーツリー参照をもたせる
    }

    nextUnitOfWork = wipRoot
}

let wipRoot = null
let currentRoot = null
let nextUnitOfWork = null;

さらには performUnitOfWork 関数内にある、子要素のファイバーを作成するコードを新たに作成した reconcileChildren関数に移動させる さらに reconcileChildren 関数内でで古いファイバーと新しい要素を比較するようにする。
この関数内で登場する element はDOMにレンダリングしたいものであり、oldFiber は前回レンダリングしたもの。
それらを比較して、DOMに適用すべき変更があるかどうかを確認していく処理を reconcileChildren内に実装していく。
(正直ここらへんが難しい)

とにかく書いてみたコードを下に載せる(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 isNew = (prev, next) => key => prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
const isEvent = key => key.startsWith("on")
const isProperty = key => key !== "children" && !isEvent(key)

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

    updateDom(dom, {}, fiber.props)

    return dom
}

const updateDom = (dom, prevProps, nextProps) => {
    // 削除、または変更したイベントリスナーを削除する
    Object.keys(prevProps)
        .filter(isEvent)
        .filter(
            key =>
                !(key in nextProps) ||
                isNew(prevProps, nextProps)(key)
        )
        .forEach(name => {
            const eventType = name
                .toLowerCase()
                .substring(2)
            dom.removeEventListener(
                eventType,
                prevProps[name]
            )
        })

    // 古いプロパティを削除する
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {
            dom[name] = ""
        })

    // 新規 or 変更したプロパティをセットする
    Object.keys(nextProps)
        .filter(isProperty)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            dom[name] = nextProps[name]
        })

    // 新しいイベントリスナーを追加
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            const eventType = name
                .toLowerCase()
                .substring(2)
            dom.addEventListener(
                eventType,
                nextProps[name]
            )
        })
}

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

    // 削除するノードを追跡するための配列が必要になるので追加
    deletions = []

    nextUnitOfWork = wipRoot
}

let wipRoot = null
let currentRoot = null
let nextUnitOfWork = null;
let deletions = null

const commitRoot = () => {
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
}

const commitWork = fiber => {
    if (!fiber) {
        return
    }

    const domParent = fiber.parent.dom

    if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
        domParent.appendChild(fiber.dom)
    } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
        updateDom(fiber.dom, fiber.alternate.props, fiber.props)
    } else if (fiber.effectTag === "DELETION") {
        domParent.removeChild(fiber.dom)
    }

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

const workLoop = deadline => {
    let shouldYield = false

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

        shouldYield = deadline.timeRemaining() < 1
    }

    if (!nextUnitOfWork && wipRoot) {
        commitRoot()
    }

    requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

const performUnitOfWork = fiber => {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }

    const elements = fiber.props.children
    reconcileChildren(fiber, elements)

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

    let nextFiber = fiber

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

        nextFiber = nextFiber.parent
    }
}

const reconcileChildren = (wipFiber, elements) => {
    let index = 0
    let oldFiber = wipFiber.alternate && wipFiber.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

        // 古いファイバーと新しい要素が同じタイプの場合、DOMノードを保持し、新しいpropsで更新するだけでOK
        if (sameType) {
            // ノードの更新処理
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }

        // タイプが異なり、新しい要素がある場合は、新しいDOMノードを作成する必要があることを意味する
        if (element && !sameType) {
            // ノードの追加処理
            newFiber = {
                type: element.type,
                props: element.props,
                dom: null,
                parent: wipFiber,
                alternate: null,
                effectTag: "PLACEMENT",
            }
        }

        // タイプが異なり、古いファイバーがある場合は、古いノードを削除する
        if (oldFiber && !sameType) {
            // 古いファイバーノードの削除処理
            oldFiber.effectTag = "DELETION"
            deletions.push(oldFiber)
        }

        if (oldFiber) {
            oldFiber = oldFiber.sibling
        }

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

        prevSibling = newFiber
        index++
    }
}

export {
    createElement,
    render
}

そして index.jsも今回の機能追加に合わせて、書き換えた。 inputフォームの値を反映するようなサンプルになっている。
(これも参照元のドキュメントの写経である)

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

const container = document.getElementById("root")

const updateValue = e => {
  rerender(e.target.value)
}

const rerender = value => {
  /** @jsxRuntime classic */
  /** @jsx createElement */
  const element = (
    <div>
      <input onInput={updateValue} value={value} />
      <h2>Hello {value}</h2>
    </div>
  )
  render(element, container)
}

rerender("World")

差分検出実装のポイント

なお、↓の処理ではReactは key も使用するため、差分検出の効率が向上しているらしい。 たとえば、子要素が要素配列内の位置を変更したことを検出するなど。
ただ、Didactではkeyの実装は行わないので、下記のようになっている。

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

ファイバーの effectTag 内の文字列を参照して処理を行うようになった。

  • PLACEMENT がある場合は、前と同じように、DOMノードを親ファイバーのノードに追加
  • DELETION の場合は、逆の操作を行い、子要素を削除
  • UPDATEの場合は、変更されたpropsで既存のDOMノードを更新する

また、updateDom関数を作成し、そちらでDOMノードの更新作業は行うようにする。

また、isNewisGone という filter内で利用する関数も新たに追加。

さらにpropsの中でイベントリスナーは更新する必要がある。
そのため、 propson で始まるpropsは検知して、異なる方法で処理する必要がある。

ここの処理は updateDom 内で行われていて、実際のコードは下記のようになる。

    // 新しいイベントリスナーを追加
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
            const eventType = name
                .toLowerCase()
                .substring(2)
            dom.addEventListener(
                eventType,
                nextProps[name]
            )
        })

考えてみれば当たり前のことなのだが、関数を設定するのに EventTarget.addEventListener() を使用している。

developer.mozilla.org

日々のコーディングの中で当たり前のように関数をコンポーネントに設定しているが、実際にdomのイベントリスナーを設定する際にはこのような処理( addEventListener を使って設定するというような基本的な処理) が裏で動いているということに気づけて、親近感が湧いた。

学んだ感想

それにしても差分検出あたりまで来ると、結構複雑になってくると感じる。

一気に読もうとすると、なかなか疲れてくるが、とりあえず大枠だけ理解できたという感じ。

これを踏まえて1から書いていけば、より理解できそうだと思った。

当たり前のように日々Reactを使っているが、裏で当然のように行われているDOM操作を見るというのはやはり大切なんだろうなと思った。

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から自分で実装しないと、意図やこのようにすることで達成できるものなどがうまく想像できない。

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

深い階層を持つHTMLを生成するための、どうでも良い関数を作ったのメモ(React)

深い階層を持つHTMLを生成するための関数を書いた。

あまり使いみちはない。
下記で書いている、Reactを自作する際に生み出した謎の副産物である。

shinshin86.hateblo.jp

以下、ソースコード

const generateDeepElement = (depth) => {
  depth = depth - 1;
  
  if(depth < 1) {
    return createElement("p", null, 'HELLO!!!!!')
  } else {
    return createElement("div", null, generateDeepElement(depth))
  }
}

const element = createElement(
  "div",
  { id: "foo" },
  createElement("h1", null, "hello"),
  createElement("p", null, createElement("b", null, "world")),
  generateDeepElement(10)
)

例えば上の処理を実行して render 関数を動かすと、下記のようなHTMLが生成される。

<div id="foo">
  <h1>hello</h1>
  <p><b>world</b></p>
  <div>
    <div>
      <div>
        <div>
          <div>
            <div>
              <div>
                <div>
                  <div>
                    <p>HELLO!!!!!</p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

繰り返すが実用度はゼロ。

せっかく書いたので残しておこう、という精神でこちらをポストした。

Reactを自作しようとしたときに"pragma and pragmaFrag cannot be set when runtime is automatic."のエラーが出たのでその対応方法を書いていく

最近、Reactの中身をちゃんと理解せねばと思い、Reactの自作につながるような勉強をしています。

一応下に備忘録を残していっていますが、個人用のメモみたいなレベルなので、あまり見やすくはないと思います。
うまく成果を得られたらQiitaかZennにまとめようと思っていますが、どうなることやら...

shinshin86.hateblo.jp

下記のドキュメントを参照しながら進めているのですが、"pragma and pragmaFrag cannot be set when runtime is automatic."というエラーが出て、
「コード同じなのになんでだろう?」
と少しばかり悩んだのでその時の解決方法などを書いていきます。

Build your own React

下は上の翻訳記事。

Reactを自作しよう

"pragma and pragmaFrag cannot be set when runtime is automatic."の発生原因

問題のコードは下記です。

ここはbableがjsxをトランスパイルした後に、 Didact.createElement で処理が行われます。
(この説明だけだと意味がわからないと思うので、上に貼った記事を読んでみてください)

/** @jsx Didact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
);

この処理自体はなんの問題もないのですが、この記事の中での環境はReact v16です。

現在、最新バージョンのReactはv17となっているのですが、実はReact 17だとこれは解釈できず、"pragma and pragmaFrag cannot be set when runtime is automatic."というエラーを吐きます。

検証手順

下記のCodeSandboxは上のドキュメント内で実際に提示されているCodeSandboxのコードです。

didact-2 - CodeSandbox

これは現在通常通り動いていますが、CodeSandbox上で Dependencies として設定されているライブラリのすべてを最新にアップデートすると、この事象が発生します。

  • react 16.8.6 → 17.02
  • react-dom 16.8.6 → 17.02
  • react-scripts 3.0.1 → 4.0.3

これを解決する手立ては、下記のissueにありました。

React 17 + new JSX transform + JSX pragma/pragmaFrag · Issue #2041 · emotion-js/emotion · GitHub

ここに記載されている記述を参照して、下記のようにコメント部分を修正することでエラーが解消されます。

/** @jsxRuntime classic */
/** @jsx Didact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
);
  • なぜ、このような書き方にする必要があるのか?
  • どこの変更によって、このような書き方にしなければならなくなったのか?

など細かい部分までは調べていませんが、取り急ぎ備忘録としてこちらに残します。