このチュートリアルはこのチュートリアルに基づいていますが、JSX、typescript、および実装がより簡単なアプローチを使用しています。私の GitHub リポジトリでメモとコードをチェックアウトできます。
この部分では、vDOM を実際の DOM にレンダリングします。さらに、React のコア構造であるファイバー ツリーについても紹介します。
vDOM のレンダリングは非常にシンプルです。シンプルすぎます。次の Web ネイティブ API について知っておく必要があります。
おお、ちょっとやりすぎですよね?ただし、必要なのは、vDOM の作成を実際の DOM にミラーリングすることだけです。以下に簡単な例を示します。
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
on で始まるプロパティをイベント リスナーとして登録しました。これは React では一般的な方法です。また、レンダリングではなく調整に使用される key プロパティも無視しました。
さて、レンダリングが完了したら、この章は終了です...?いいえ。
実際の React では、レンダリング プロセスはもう少し複雑です。具体的には、requestIdleCallback を使用して、より緊急性の高いタスクを最初に実行し、自身の優先順位を下げます。
requestIdleCallback は、Safari、MacOS と iOS の両方でサポートされていないことに注意してください (Apple エンジニア、なぜですか? 少なくとも 2024 年には開発中です)。 Mac を使用している場合は、Chrome を使用するか、単純な setTimeout に置き換えてください。実際の React では、スケジューラーを使用してこれを処理しますが、基本的な考え方は同じです。
これを行うには、次の Web ネイティブ API を理解する必要があります。
したがって、レンダリングをチャンクに分割し、requestIdleCallback を使用して処理する必要があります。簡単な方法は、一度に 1 つのノードをレンダリングすることです。これは簡単ですが、積極的に実行しないでください。そうしないと、レンダリング中に他の作業も行う必要があるため、多くの時間を無駄にすることになります。
しかし、これから行うことの基本的なフレームワークとして次のコードを使用できます。
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
ここで // TODO に vDOM のレンダリングを入力し、次にレンダリングされる vDOM ノードを返すと、単純なアイドル時間のレンダリングが可能になります。しかし、焦らないでください。さらに作業が必要です。
次の章では、反応性を実装します。調整はかなり複雑です。そのため、一部のコンテンツをこの部分、つまりファイバー ツリーに移動します。
ファイバーツリーは単なる特別なデータ構造です。 React は変更を処理するときに次のプロセスを実行します。
ご覧のとおり、ファイバー ツリーは React に不可欠です。
ファイバーツリーは従来のツリーとは少し異なり、ノード間に 3 種類の関係があります。
たとえば、次の DOM の場合、
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
それを木として表すことができます。
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
p はルート div の子ですが、セカンダリ div はルート div の子ではなく、p の兄弟です。 h1 と h2 は 2 次 div の子です。
レンダリングに関しては、順序は主に深さ優先ですが、少し異なります。そのため、基本的には次のルールに従います。ノードごとに、次の手順が実行されます。
それではそれを実装しましょう。ただし、最初にレンダリング プロセスをトリガーする必要があります。これは簡単です。nextUnitOfWork をファイバー ツリーのルートに設定するだけです。
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
レンダリングをトリガーした後、ブラウザはperformUnitOfWorkを呼び出します。ここで作業を実行します。
1 つ目は、実際の DOM 要素を作成する必要があるということです。これを行うには、新しい DOM 要素を作成し、それを親 DOM 要素に追加します。
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
これは作品の最初の部分です。次に、現在のファイバーから分岐するファイバーを構築する必要があります。
div ├── p └── div ├── h1 └── h2
これで、現在のノード用にファイバー ツリーが構築されました。それでは、ルールに従ってファイバーツリーを加工しましょう。
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
これで、vDOM をレンダリングできます。これが表示されます。 typescript は仮想 DOM の型を伝えることができないため、ここでは愚かであることに注意してください。ここでは醜いバイパスが必要です。
function isString(value: VDomNode): value is string { return typeof value === 'string' } function isElement(value: VDomNode): value is VDomElement { return typeof value === 'object' } export function createDom(vDom: VDomNode): HTMLElement | Text | DocumentFragment { if (isString(vDom)) { return document.createTextNode(vDom) } else if (isElement(vDom)) { const element = document.createElement(vDom.tag === '' ? 'div' : vDom.tag) Object.entries(vDom.props ?? {}).forEach(([name, value]) => { if (value === undefined) return if (name === 'key') return if (name.startsWith('on') && value instanceof Function) { element.addEventListener(name.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(name, value.toString()) } }) return element } else { throw new Error('Unexpected vDom type') } }
これで、vDOM が実際の DOM にレンダリングされます。おめでとう!素晴らしい仕事をしてくれました。しかし、まだ終わっていません。
現在の実装には問題が発生します。ノードが多すぎてプロセス全体が遅くなると、ユーザーはレンダリングがどのように行われるかを確認することになります。もちろん、商業機密などが漏洩するわけではありませんが、良い経験ではありません。私たちは、DOM の作成をカーテンの後ろに隠し、一度にすべて送信することを希望します。
解決策は簡単です。ドキュメントに直接コミットする代わりに、要素をドキュメントに追加せずに作成し、完了したらそれをドキュメントに追加します。これは累積コミットと呼ばれます。
function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { if(!nextUnitOfWork) { return null } if(!nextUnitOfWork.dom) { nextUnitOfWork.dom = createDom(nextUnitOfWork.vDom) } if(nextUnitOfWork.parent && nextUnitOfWork.parent.dom) { nextUnitOfWork.parent.dom.appendChild(nextUnitOfWork.dom) } // TODO throw new Error('Not implemented') }
ここで、performUnitOfWork から appendChild、つまり次の部分を削除します。
const fiber = nextUnitOfWork if (isElement(fiber.vDom)) { const elements = fiber.vDom.children ?? [] let index = 0 let prevSibling = null while (index < elements.length) { const element = elements[index] const newFiber: Fiber = { parent: fiber, dom: null, sibling: null, child: null, vDom: element, } if (index === 0) { fiber.child = newFiber } else { prevSibling!.sibling = newFiber } prevSibling = newFiber index++ } }
すべての作業が完了すると、すべてのファイバーが DOM で正しく構築されますが、ドキュメントには追加されません。このようなイベントが送出されると、コミット関数が呼び出され、DOM がドキュメントに追加されます。
if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null
コミット関数は単純です。すべての子 DOM を再帰的に Wip に追加し、Wip を DOM にコミットするだけです。
import { render } from "./runtime"; import { createElement, fragment, VDomNode } from "./v-dom"; function App() { return <> <h1>a</h1> <h2>b</h2> </> } const app = document.getElementById('app') const vDom: VDomNode = App() as unknown as VDomNode render(vDom, app!)
commitChildren 関数にタイムアウトを追加することで、これをテストできます。以前は、レンダリングは段階的に行われていましたが、現在はすべて一度に行われます。
次のようなネストされた関数を試してみることもできます。
function render(vDom: VDomNode, parent: HTMLElement) { if (typeof vDom === 'string') { parent.appendChild(document.createTextNode(vDom)) } else if (vDom.kind === 'element') { const element = document.createElement(vDom.tag) for (const [key, value] of Object.entries(vDom.props ?? {})) { if (key === 'key') continue if (key.startsWith('on')) { element.addEventListener(key.slice(2).toLowerCase(), value as EventListener) } else { element.setAttribute(key, value as string) } } for (const child of vDom.children ?? []) { render(child, element) } parent.appendChild(element) } else { for (const child of vDom.children ?? []) { render(child, parent) } } }
しかし、JSX を解析するとき、タグは単なるラベル名であるため、これは機能しません。確かに、ネイティブ要素の場合は単なる文字列ですが、コンポーネントの場合は関数です。したがって、JSX を vDOM に変換するプロセスでは、タグが関数であるかどうかを確認し、関数である場合はそれを呼び出す必要があります。
import { createDom, VDomNode } from "./v-dom" interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode, dom: HTMLElement | Text | null } let nextUnitOfWork: Fiber | null = null function workLoop(deadline: IdleDeadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null { // TODO throw new Error('Not implemented') }
ここで、各コンポーネントにプロップと子が必要になります。実際の React では、チェックする追加のフィールドが追加されました。想像できると思いますが、関数をクラスに置き換えるだけで、追加のフィールドができます。その後、オブジェクトを作成するための新しい関数を提供します (典型的なファクトリ パターン)。しかし、ここでは怠惰な方法を採用します。
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
実際の React では、関数コンポーネントの呼び出しがファイバー構築段階まで遅れることに注意してください。それにもかかわらず、これは便宜上そうしたものであり、このシリーズの目的を実際に損なうものではありません。
しかし、それでもまだ十分ではありません。以前は、フラグメントを div として扱うだけでしたが、これは正しくありません。しかし、それを文書の断片に置き換えるだけでは機能しません。その理由は、フラグメントは 1 回限りのコンテナであるためです。フラグメントから実際のものを取り出すことはできず、ネストすることもできません。また、多くの奇妙な動作が発生します (本当に、なぜうまくいかないのでしょう)もっと簡単に動作しないでしょうか...)。それで、くそー、これを掘り起こさなければなりません。
したがって、解決策は、フラグメントの DOM を作成せず、DOM を追加する正しい親を見つけることです。
必要です
div ├── p └── div ├── h1 └── h2
そしてレンダリングを変更します。
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
これで、フラグメントが正しく処理されるようになりました。
以上が小さな React Chendering vDOM を構築するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。