Dieses Tutorial basiert auf diesem Tutorial, jedoch mit JSX, Typoskript und einem einfacheren Ansatz zur Implementierung. Sie können sich die Notizen und den Code in meinem GitHub-Repo ansehen.
In diesem Teil rendern wir das vDOM in das tatsächliche DOM. Darüber hinaus werden wir auch den Faserbaum vorstellen, eine Kernstruktur in React.
Das Rendern von vDOM ist einfach – zu einfach. Sie müssen die folgenden webnativen APIs kennen.
Woa, ein bisschen zu viel, oder? Sie müssen jedoch lediglich die Erstellung von vDOM auf das tatsächliche DOM spiegeln. Hier ist ein einfaches Beispiel.
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) } } }
Wir haben Eigenschaften, die mit „on“ beginnen, als Ereignis-Listener registriert. Dies ist eine gängige Praxis in React. Außerdem haben wir die Schlüsseleigenschaft ignoriert, die zum Abgleich und nicht zum Rendern verwendet wird.
Okay, das Rendern ist also abgeschlossen und dieses Kapitel endet...? Nein.
Im echten React ist der Rendervorgang etwas komplizierter. Genauer gesagt wird requestIdleCallback verwendet, um dringendere Aufgaben zuerst zu erledigen, wodurch die eigene Priorität gesenkt wird.
Bitte beachten Sie, dass requestIdleCallback in Safari, sowohl auf MacOS als auch auf iOS, nicht unterstützt wird (Apple-Ingenieure, bitte, warum? Zumindest arbeiten sie daran, im Jahr 2024). Wenn Sie einen Mac verwenden, verwenden Sie Chrome oder ersetzen Sie es durch ein einfaches setTimeout. In der realen Reaktion wird hierfür ein Scheduler verwendet, aber die Grundidee ist dieselbe.
Dazu müssen wir die folgenden webnativen APIs kennen.
Also müssen wir unser Rendering in Blöcke aufteilen und requestIdleCallback verwenden, um es zu verarbeiten. Eine einfache Möglichkeit wäre, jeweils nur einen Knoten zu rendern. Es ist einfach – aber seien Sie nicht begierig darauf – sonst verschwenden Sie viel Zeit, da beim Rendern noch andere Arbeiten erledigt werden müssen.
Aber wir können den folgenden Code als Grundgerüst für das haben, was wir tun werden.
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) } } }
Wenn Sie // TODO jetzt mit dem Rendern von vDOM füllen und den nächsten zu rendernden vDOM-Knoten zurückgeben, können Sie ein einfaches Leerlaufzeit-Rendering durchführen. Aber seien Sie nicht voreilig – wir brauchen mehr Arbeit.
Im nächsten Kapitel werden wir Reaktivität implementieren, und der Abgleich ist ziemlich kompliziert – daher verschieben wir einige Inhalte in diesen Teil, den Faserbaum.
Faserbaum ist nur eine spezielle Datenstruktur. Wenn React Änderungen verarbeitet, führt es den folgenden Prozess aus.
Sie sehen, der Faserbaum ist für React unerlässlich.
Der Faserbaum unterscheidet sich ein wenig vom herkömmlichen Baum und weist drei Arten von Beziehungen zwischen Knoten auf.
Zum Beispiel für das folgende 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') }
Wir können es als Baum darstellen.
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
p ist ein Kind des Root-Divs, aber das sekundäre Div ist kein Kind des Root-Divs, sondern ein Geschwister von p. h1 und h2 sind Kinder des sekundären Div.
Beim Rendern ist die Reihenfolge hauptsächlich auf die Tiefe ausgerichtet, aber etwas anders – im Grunde folgt es also diesen Regeln. Für jeden Knoten werden die folgenden Schritte durchlaufen.
Jetzt lasst uns das umsetzen. Aber zuerst müssen wir den Rendervorgang auslösen. Es ist ganz einfach: Setzen Sie einfach nextUnitOfWork auf die Wurzel des Faserbaums.
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) } } }
Nachdem das Rendern ausgelöst wurde, ruft der Browser performUnitOfWork auf. Hier führen wir die Arbeit aus.
Das erste ist, dass wir tatsächliche DOM-Elemente erstellen müssen. Wir können dies tun, indem wir ein neues DOM-Element erstellen und es an das übergeordnete DOM-Element anhängen.
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>
Dies ist der erste Teil der Arbeit. Jetzt müssen wir die Faser konstruieren, die von der aktuellen abzweigt.
div ├── p └── div ├── h1 └── h2
Jetzt haben wir einen Faserbaum für den aktuellen Knoten erstellt. Befolgen wir nun unsere Regeln zur Verarbeitung des Faserbaums.
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
Jetzt können wir das vDOM rendern, hier ist es. Bitte beachten Sie, dass Typoskript hier dumm ist, da es den Typ unseres virtuellen DOM nicht erkennen kann. Wir brauchen hier einen hässlichen Bypass.
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') } }
Jetzt wird Ihr vDOM in das eigentliche DOM gerendert. Glückwunsch! Du hast einen tollen Job gemacht. Aber wir sind noch nicht fertig.
Es wird ein Problem mit der aktuellen Implementierung geben – wenn wir zu viele Knoten haben, was den gesamten Prozess verlangsamt, wird der Benutzer sehen, wie das Rendering erfolgt. Natürlich werden dadurch keine Geschäftsgeheimnisse oder ähnliches preisgegeben, aber es ist keine gute Erfahrung. Wir möchten die Dom-Kreation lieber hinter dem Vorhang verstecken und alles auf einmal einreichen.
Die Lösung ist einfach: Anstatt uns direkt auf das Dokument festzulegen, erstellen wir ein Element, ohne es dem Dokument hinzuzufügen, und wenn wir fertig sind, fügen wir es dem Dokument hinzu. Dies wird als kumulatives Commit bezeichnet.
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') }
Jetzt entfernen wir das appendChild aus performUnitOfWork, also den folgenden Teil,
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++ } }
Wenn wir nun die gesamte Arbeit abgeschlossen haben, haben wir alle Fasern korrekt mit ihrem DOM aufgebaut, sie werden jedoch nicht zum Dokument hinzugefügt. Wenn ein solches Ereignis ausgelöst wird, rufen wir eine Commit-Funktion auf, die das DOM zum Dokument hinzufügt.
if (fiber.child) { return fiber.child } let nextFiber: Fiber | null = fiber while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling } nextFiber = nextFiber.parent } return null
Jetzt ist die Commit-Funktion einfach: Fügen Sie einfach alle untergeordneten DOMs rekursiv zum WIP hinzu und übergeben Sie dann das WIP an das 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!)
Sie können dies testen, indem Sie der commitChildren-Funktion eine Zeitüberschreitung hinzufügen. Früher wurde das Rendern Schritt für Schritt durchgeführt, jetzt erfolgt es jedoch auf einmal.
Sie können verschachtelte Funktionen wie die folgenden ausprobieren
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) } } }
Aber es wird nicht funktionieren, da beim Parsen des JSX das Tag nur der Labelname ist. Sicher, bei nativen Elementen ist es nur eine Zeichenfolge, aber bei Komponenten ist es eine Funktion. Bei der Konvertierung von JSX in vDOM müssen wir also prüfen, ob das Tag eine Funktion ist, und wenn ja, rufen Sie es auf.
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') }
Jetzt sind für jede Komponente Requisiten und Kinder erforderlich. In echtem React haben sie ein zusätzliches Feld zur Überprüfung hinzugefügt – Sie können sich das vorstellen, indem Sie einfach Funktionen durch Klassen ersetzen, sodass Sie zusätzliche Felder haben – und dann neue Funktionen zum Erstellen von Objekten bereitstellen, ein typisches Fabrikmuster – aber wir nehmen hier ein faules Gefühl.
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
Bitte beachten Sie, dass im echten React der Aufruf der Funktionskomponente auf die Phase des Faseraufbaus verzögert wird. Nichtsdestotrotz haben wir dies aus Bequemlichkeitsgründen getan und es schadet dem Zweck dieser Serie nicht wirklich.
Allerdings ist es immer noch nicht genug. Bisher haben wir Fragmente nur als div behandelt, was nicht korrekt ist. Aber wenn Sie das einfach durch ein Dokumentfragment ersetzen, wird es nicht funktionieren. Der Grund dafür ist, dass es sich bei Fragmenten um einen einmaligen Container handelt – was zu einem seltsamen Verhalten führt – zum Beispiel, dass man keine echten Dinge daraus herausnehmen und sie nicht verschachteln kann, und viele seltsame Dinge (wirklich, warum hat es einfach gewonnen?) Einfacher geht es nicht...). Also, verdammt, wir müssen diesen Scheiß ausgraben.
Die Lösung besteht also darin, dass wir kein DOM für das Fragment erstellen, sondern das richtige übergeordnete Element finden, um das DOM hinzuzufügen.
Wir brauchen,
div ├── p └── div ├── h1 └── h2
Und ändern Sie das Rendering,
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
Jetzt wird das Fragment korrekt verarbeitet.
Das obige ist der detaillierte Inhalt vonErstellen Sie ein Tiny React Chendering vDOM. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!