Heim  >  Artikel  >  Web-Frontend  >  Erstellen Sie ein Tiny React Chendering vDOM

Erstellen Sie ein Tiny React Chendering vDOM

Patricia Arquette
Patricia ArquetteOriginal
2024-10-20 18:28:30663Durchsuche

Build a Tiny React Chendering vDOM

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.

vDOM rendern

Das Rendern von vDOM ist einfach – zu einfach. Sie müssen die folgenden webnativen APIs kennen.

  • document.createElement(tagName: string): HTMLElement Erstellt ein tatsächliches DOM-Element.
  • document.createTextNode(text: string): Text Erstellt einen Textknoten.
  • .appendChild(child: Node): void Hängt einen untergeordneten Knoten an den übergeordneten Knoten an. Eine Methode für HTMLElement
  • .removeChild(child: Node): void Entfernt einen untergeordneten Knoten vom übergeordneten Knoten. Eine Methode für HTMLElement
  • .replaceChild(newChild: Node, oldChild: Node): void Ersetzt einen untergeordneten Knoten durch einen neuen untergeordneten Knoten. Eine Methode für HTMLElement
  • .replaceWith(...nodes: Node[]): void Ersetzt einen Knoten durch neue Knoten. Eine Methode auf Node
  • .remove(): void Entfernt einen Knoten aus dem Dokument. Eine Methode auf Node
  • .insertBefore(newChild: Node, refChild: Node): void Fügt einen neuen untergeordneten Knoten vor einem Referenz-untergeordneten Knoten ein. Eine Methode für HTMLElement
  • .setAttribute(name: string, value: string): void Legt ein Attribut für ein Element fest. Eine Methode für HTMLElement.
  • .removeAttribute(name: string): void Entfernt ein Attribut aus einem Element. Eine Methode für HTMLElement.
  • .addEventListener(type: string, listener: Function): void Fügt einem Element einen Ereignis-Listener hinzu. Eine Methode für HTMLElement.
  • .removeEventListener(type: string, listener: Function): void Entfernt einen Event-Listener aus einem Element. Eine Methode für HTMLElement.
  • .dispatchEvent(event: Event): void Löst ein Ereignis für ein Element aus. Eine Methode für HTMLElement.

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.

Leerlaufzeit-Rendering

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.

  • requestIdleCallback(callback: Function): void Fordert einen Rückruf an, der aufgerufen werden soll, wenn der Browser inaktiv ist. Dem Rückruf wird ein IdleDeadline-Objekt übergeben. Der Rückruf verfügt über ein Deadline-Argument, bei dem es sich um ein Objekt mit den folgenden Eigenschaften handelt.
    • timeRemaining(): Zahl Gibt die verbleibende Zeit in Millisekunden zurück, bevor der Browser nicht mehr inaktiv ist. Wir sollten also unsere Arbeit beenden, bevor die Zeit abgelaufen ist.

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.

Faserbaum

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.

  1. Etwas, möglicherweise ein Benutzer oder ein anfängliches Rendering, löst eine Änderung aus.
  2. React erstellt einen neuen vDOM-Baum.
  3. Reagieren Sie, berechnen Sie den neuen Faserbaum.
  4. React berechnet die Differenz zwischen dem alten Faserbaum und dem neuen Faserbaum.
  5. React wendet die Differenz auf das tatsächliche DOM an.

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.

  • Kind von: Ein Knoten ist ein Kind eines anderen Knotens. Bitte beachten Sie, dass im Faserbaum jeder Knoten nur ein untergeordnetes Element haben kann. Die traditionelle Baumstruktur wird durch ein Kind mit vielen Geschwistern dargestellt.
  • Geschwister von: Ein Knoten ist ein Geschwister eines anderen Knotens.
  • Elternteil von: Ein Knoten ist ein Elternteil eines anderen Knotens. Im Gegensatz zu den untergeordneten Knoten können viele Knoten denselben übergeordneten Knoten haben. Sie können sich den übergeordneten Knoten im Faserbaum als einen schlechten Elternteil vorstellen, der sich nur um das erste Kind kümmert, aber dennoch der Elternteil vieler Kinder ist.

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.

  1. Wenn dieser Knoten ein unverarbeitetes untergeordnetes Element hat, verarbeiten Sie das untergeordnete Element.
  2. Wenn dieser Knoten einen Geschwisterknoten hat, verarbeiten Sie den Geschwisterknoten. Wiederholen, bis alle Geschwister verarbeitet sind.
  3. Diesen Knoten als verarbeitet markieren.
  4. Verarbeiten Sie das übergeordnete Element.

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.

Kumulatives Commit

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.

Verschachtelte Komponenten

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.

Fragment

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn