Reactを自作するための学習の記録の続き
この続き
参照ドキュメント
現在ここに記述しているコードは、下記のドキュメントのほぼ写経となっています。
おさらい
ここまでの実装は下記。
なお、createElement
や render
は参照ドキュメントに倣い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
の箇所については下記に別途書いたので、気になる方は見てみてください。
再帰呼び出し部分の修正(render関数)
次は再帰呼び出し部分の修正を行っていく。
現在の実装では、要素ツリーをレンダリングし終えるまで停止しない実装となっている。
const render = (element, container) => { ・ ・ ・ // ここの部分 element.props.children.forEach(child => render(child, dom) ) ・ ・ ・ }
そのため、ここの処理を改修していく。
各ユニットが終了した後、他に実行させる必要があることがあれば、ブラウザにレンダリングを中断させるなど、柔軟な処理が行えるようにしていく。
ここで登場するのが requestIdleCallback
というWeb API。
メインスレッドがアイドル状態のときにブラウザがコールバックを実行してくれるらしい。
また、requestIdleCallback
はtimeoutのパラメーターも扱えるようで、これを使用して、ブラウザが再び制御する必要があるまでの時間を制御できるようだ。
なお、現在のReactの実装ではこの関数は利用しておらず、別のスケジューラパッケージを利用しているとのことだが、概念としては同じようだ。
Fiberについて
また、ここではファイバーツリーというデータ構造も出てくる。
詳細は下記に記載されている。
また、下記の日本語の記事は非常にわかりやすかった。
このFiberというのは、Reactの更新処理に優先度が付けられるようにするために設定された作業の構成単位のことらしい。
といっても、うまく理解できないので、もう少し細かく見ていく。
Fiberはelementごとに一本のファイバーがあり、これら1本1本のファイバーが作業単位になるらしい。
作業単位、という言葉が出てきたが、具体的には
- 要素をDOMに追加する
- 子要素のためのファイバーを新たに作成する
- 次の作業単位(ファイバーのこと)を選択する
ということを行うようだ。
ちなみにこのファイバーを用いたデータ構造をファイバーツリーというらしい。
このファイバーツリー自体の目的は、次に作業を行うべき要素を簡単に見つけられるようにすることらしい。
またこのことを実現するためにはファイバーには
- 最初の子(child)
- 次の兄弟(sibling) →
sibling
という単語にあまり馴染みがなかったが、兄弟のことだ - 親(parent)
という3つのリンクを持つようだ。
例えば下記の構造を処理していきたいとする。
(この構造は参照しているページの構造を引用している)
<div> <h1> <p /> <a /> </h1> <h2 /> </div>
この場合、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から自分で実装しないと、意図やこのようにすることで達成できるものなどがうまく想像できない。
一旦ここについてはこのまま進めて、また後ほど紐解いていってみようと思う。