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

Erstellen Sie ein Tiny React Chpdating vDOM

Linda Hamilton
Linda HamiltonOriginal
2024-10-20 18:31:30145Durchsuche

Build a Tiny React Chpdating 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.

Jetzt reden wir über die Reaktivität.

Bewahren Sie die alte Faser auf

Wir müssen die alte Faser aufbewahren, damit wir sie mit der neuen Faser vergleichen können. Wir können dies tun, indem wir der Faser ein Feld hinzufügen. Wir brauchen auch ein engagiertes Feld, das später nützlich sein wird.

export interface Fiber {
  type: string
  props: VDomAttributes
  parent: Fiber | null
  child: Fiber | null
  sibling: Fiber | null
  dom: HTMLElement | Text | null
  alternate: Fiber | null
  committed: boolean
}

Dann legen wir hier den Committed-Status fest,

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent.dom.appendChild(fiber.dom)
            fiber.committed = true
        }
        if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) {
            let parent = fiber.parent
            // find the first parent that is not a fragment
            while(parent && isFragment(parent.vDom)) {
                // the root element is guaranteed to not be a fragment has has a non-fragment parent
                parent = parent.parent!
            }
            parent.dom?.appendChild(fiber.dom!)
            fiber.committed = true
        }
        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    wip = null
}

Wir müssen auch den alten Faserbaum retten.

let oldFiber: Fiber | null = null

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent.dom.appendChild(fiber.dom)
            fiber.committed = true
        }
        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    oldFiber = wip
    wip = null
}

Jetzt müssen wir während der Iteration die alte Faser mit der neuen Faser vergleichen. Dies wird als Versöhnungsprozess bezeichnet.

Versöhnung

Wir müssen die alte Faser mit der neuen Faser vergleichen. Wir haben zuerst die alte Faser in die erste Arbeit eingebracht.

export function render(vDom: VDomNode, parent: HTMLElement) {
    wip = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: null,
        committed: false,
        alternate: oldFiber,
    }
    wipParent = parent
    nextUnitOfWork = wip
}

Dann trennen wir die Entstehung der neuen Faser in eine neue Funktion.

function reconcile(fiber: Fiber, isFragment: boolean) {
    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: isFragment ? fiber.parent : fiber,
                dom: null,
                sibling: null,
                child: null,
                vDom: element,
                committed: false,
                alternate: null,
            }

            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling!.sibling = newFiber
            }
            prevSibling = newFiber
            index++
        }
    }
}

function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null {
    if(!nextUnitOfWork) {
        return null
    }
    const fiber = nextUnitOfWork
    const isFragment = isElement(fiber.vDom) && fiber.vDom.tag === '' && fiber.vDom.kind === 'fragment'

    if(!fiber.dom && !isFragment) {
        fiber.dom = createDom(fiber.vDom)
    }

    reconcile(fiber, isFragment)

    if (fiber.child) {
        return fiber.child
    }
    let nextFiber: Fiber | null = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
          return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
    return null
}

Allerdings müssen wir die alte Faser auf die neue montieren.

function reconcile(fiber: Fiber, isFragment: boolean) {
    if (isElement(fiber.vDom)) {
        const elements = fiber.vDom.children ?? []
        let index = 0
        let prevSibling = null

        let currentOldFiber = fiber.alternate?.child ?? null
        while (index < elements.length) {
            const element = elements[index]
            const newFiber: Fiber = {
                parent: isFragment ? fiber.parent : fiber,
                dom: null,
                sibling: null,
                child: null,
                vDom: element,
                committed: false,
                alternate: currentOldFiber,
            }

            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling!.sibling = newFiber
            }
            prevSibling = newFiber
            currentOldFiber = currentOldFiber?.sibling ?? null
            index++
        }
    }
}

Jetzt haben wir die alte Faser an der neuen Faser montiert. Aber wir haben nichts, was das erneute Rendern auslösen könnte – im Moment lösen wir es manuell aus, indem wir eine Schaltfläche hinzufügen. Da wir noch keinen Status haben, verwenden wir Requisiten zum Mutieren des vDOM.

import { render } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

type FuncComponent = (props: VDomAttributes, children: VDomNode[]) => JSX.Element

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    return <div>
        <>
            <h1>H1</h1>
            <h2>{props["example"]?.toString()}</h2>
            {
                props["show"] ? <p>show</p> : <></>
            }
            <h1>H1</h1>
        </>
    </div>
}
const app = document.getElementById('app')

const renderButton = document.createElement('button')
renderButton.textContent = 'Render'
let cnt = 0
renderButton.addEventListener('click', () => {
    const vDom: VDomNode = App({
        "example": (new Date()).toString(),
        "show": cnt % 2 === 0
    }, []) as unknown as VDomNode
    cnt++
    render(vDom, app!)
})
document.body.appendChild(renderButton)

Wenn Sie nun auf den RenderButton klicken, wird das gerenderte Ergebnis einmal wiederholt, da unsere aktuelle Logik einfach darin besteht, das gerenderte vDOM in das Dokument einzufügen.

Wenn Sie in der Commit-Funktion ein console.log hinzufügen, können Sie sehen, wie die alternative Faser ausgedruckt wird.

Jetzt müssen wir definieren, wie wir mit der alten und der neuen Faser umgehen und das DOM basierend auf den Informationen mutieren. Die Regeln lauten wie folgt.

Für jede neue Faser,

  • Wenn es eine alte Fiber gab, vergleichen wir den Inhalt der alten Fiber mit dem neuen Fiber. Wenn sie unterschiedlich sind, ersetzen wir den alten DOM-Knoten durch den neuen DOM-Knoten, oder wir kopieren den alten DOM-Knoten in den neuer DOM-Knoten. Bitte beachten Sie, dass mit der Gleichheit zweier vDOMs gemeint ist, dass ihre Tags und alle Eigenschaften gleich sind. Ihre Kinder können unterschiedlich sein.
  • Wenn keine alte Faser vorhanden ist, erstellen wir einen neuen DOM-Knoten und hängen ihn an den übergeordneten Knoten an.
  • Wenn die neue Faser kein Kind oder Geschwister hat, die alte Faser jedoch ein Kind oder Geschwister hat, entfernen wir rekursiv das alte Kind oder Geschwister.

Irgendwie verwirrt? Nun, ich zeige nur den Code. Wir löschen zunächst die alte DOM-Erstellung. Dann wenden Sie die oben genannten Regeln an.

Die erste Regel: Wenn eine alte Faser vorhanden ist, vergleichen wir den Gehalt der alten Faser mit dem neuen Fasergehalt. Wenn sie unterschiedlich sind, ersetzen wir den alten DOM-Knoten durch den neuen DOM-Knoten oder kopieren den alten DOM-Knoten auf den neuen DOM-Knoten.

export function vDOMEquals(a: VDomNode, b: VDomNode): boolean {
    if (isString(a) && isString(b)) {
        return a === b
    } else if (isElement(a) && isElement(b)) {
        let ret = a.tag === b.tag && a.key === b.key
        if (!ret) return false
        if (a.props && b.props) {
            const aProps = a.props
            const bProps = b.props
            const aKeys = Object.keys(aProps)
            const bKeys = Object.keys(bProps)
            if (aKeys.length !== bKeys.length) return false
            for (let i = 0; i < aKeys.length; i++) {
                const key = aKeys[i]
                if (key === 'key') continue
                if (aProps[key] !== bProps[key]) return false
            }
            for (let i = 0; i < bKeys.length; i++) {
                const key = bKeys[i]
                if (key === 'key') continue
                if (aProps[key] !== bProps[key]) return false
            }
            return true
        } else {
            return a.props === b.props
        }
    } else {
        return false
    }
}

Dann habe ich eine kleine Umgestaltung vorgenommen

export interface Fiber {
  type: string
  props: VDomAttributes
  parent: Fiber | null
  child: Fiber | null
  sibling: Fiber | null
  dom: HTMLElement | Text | null
  alternate: Fiber | null
  committed: boolean
}

Wenn es um Commit geht, haben wir jetzt ein zusätzliches alternatives Feld, um die alte Faser mit der neuen Faser zu vergleichen.

Dies ist die ursprüngliche Commit-Funktion

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent.dom.appendChild(fiber.dom)
            fiber.committed = true
        }
        if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) {
            let parent = fiber.parent
            // find the first parent that is not a fragment
            while(parent && isFragment(parent.vDom)) {
                // the root element is guaranteed to not be a fragment has has a non-fragment parent
                parent = parent.parent!
            }
            parent.dom?.appendChild(fiber.dom!)
            fiber.committed = true
        }
        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    wip = null
}

Wir werden den Namen etwas ändern. Der alte Name ist einfach falsch (das tut mir leid).

let oldFiber: Fiber | null = null

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent.dom.appendChild(fiber.dom)
            fiber.committed = true
        }
        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    oldFiber = wip
    wip = null
}

Anhängen, Kopieren und Ersetzen

Was sollen wir also tun? Unsere alte Logik hängt nur an, also extrahieren wir das

export function render(vDom: VDomNode, parent: HTMLElement) {
    wip = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: null,
        committed: false,
        alternate: oldFiber,
    }
    wipParent = parent
    nextUnitOfWork = wip
}

Wir müssen den Aufbau des DOM bis zur Commit-Phase verzögern, um mehr Flexibilität zu bieten.

function reconcile(fiber: Fiber, isFragment: boolean) {
    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: isFragment ? fiber.parent : fiber,
                dom: null,
                sibling: null,
                child: null,
                vDom: element,
                committed: false,
                alternate: null,
            }

            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling!.sibling = newFiber
            }
            prevSibling = newFiber
            index++
        }
    }
}

function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null {
    if(!nextUnitOfWork) {
        return null
    }
    const fiber = nextUnitOfWork
    const isFragment = isElement(fiber.vDom) && fiber.vDom.tag === '' && fiber.vDom.kind === 'fragment'

    if(!fiber.dom && !isFragment) {
        fiber.dom = createDom(fiber.vDom)
    }

    reconcile(fiber, isFragment)

    if (fiber.child) {
        return fiber.child
    }
    let nextFiber: Fiber | null = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
          return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
    return null
}

Der ersten und zweiten Regel folgend, wandeln wir sie in den folgenden Code um,

function reconcile(fiber: Fiber, isFragment: boolean) {
    if (isElement(fiber.vDom)) {
        const elements = fiber.vDom.children ?? []
        let index = 0
        let prevSibling = null

        let currentOldFiber = fiber.alternate?.child ?? null
        while (index < elements.length) {
            const element = elements[index]
            const newFiber: Fiber = {
                parent: isFragment ? fiber.parent : fiber,
                dom: null,
                sibling: null,
                child: null,
                vDom: element,
                committed: false,
                alternate: currentOldFiber,
            }

            if (index === 0) {
                fiber.child = newFiber
            } else {
                prevSibling!.sibling = newFiber
            }
            prevSibling = newFiber
            currentOldFiber = currentOldFiber?.sibling ?? null
            index++
        }
    }
}

Bitte beachten Sie immer, dass in Javascript alle Werte Referenzen sind. Wenn wir „fiber.dom“ = „fiber.alternate.dom“ haben, verweisen „fibre.dom“ und „fiber.alternate.dom“ auf dasselbe Objekt. Wenn wir Fiber.dom ändern, ändert sich auch Fiber.alternate.dom und umgekehrt. Deshalb haben wir beim Ersetzen einfach fiber.alternate.dom?.replaceWith(fiber.dom) verwendet. Dadurch wird das alte DOM durch das neue DOM ersetzt. Während frühere Eltern, wenn sie kopiert werden, die Datei „fibre.alternate.dom“ für ihr DOM haben, wird auch ihr DOM ersetzt.

Allerdings hatten wir die Löschung noch nicht erledigt.

Einige Pannen

Okay, der vorherige Code enthält einige Fehler, die mir beim Schreiben komplexerer JSX aufgefallen sind. Bevor wir die Löschung implementieren, beheben wir sie.

Zuvor gab es einen Fehler – wir können die Liste nicht an Requisiten weitergeben, nutzen wir diese Chance, um ihn zu beheben.

import { render } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

type FuncComponent = (props: VDomAttributes, children: VDomNode[]) => JSX.Element

const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => {
    return <div>
        <>
            <h1>H1</h1>
            <h2>{props["example"]?.toString()}</h2>
            {
                props["show"] ? <p>show</p> : <></>
            }
            <h1>H1</h1>
        </>
    </div>
}
const app = document.getElementById('app')

const renderButton = document.createElement('button')
renderButton.textContent = 'Render'
let cnt = 0
renderButton.addEventListener('click', () => {
    const vDom: VDomNode = App({
        "example": (new Date()).toString(),
        "show": cnt % 2 === 0
    }, []) as unknown as VDomNode
    cnt++
    render(vDom, app!)
})
document.body.appendChild(renderButton)

Dann korrigieren Sie einfach die Typ-Dinge – für mich gibt es nur einen Fehler, also machen Sie es bitte selbst.

Wenn wir jedoch den folgenden Code haben,

export function vDOMEquals(a: VDomNode, b: VDomNode): boolean {
    if (isString(a) && isString(b)) {
        return a === b
    } else if (isElement(a) && isElement(b)) {
        let ret = a.tag === b.tag && a.key === b.key
        if (!ret) return false
        if (a.props && b.props) {
            const aProps = a.props
            const bProps = b.props
            const aKeys = Object.keys(aProps)
            const bKeys = Object.keys(bProps)
            if (aKeys.length !== bKeys.length) return false
            for (let i = 0; i < aKeys.length; i++) {
                const key = aKeys[i]
                if (key === 'key') continue
                if (aProps[key] !== bProps[key]) return false
            }
            for (let i = 0; i < bKeys.length; i++) {
                const key = bKeys[i]
                if (key === 'key') continue
                if (aProps[key] !== bProps[key]) return false
            }
            return true
        } else {
            return a.props === b.props
        }
    } else {
        return false
    }
}

Unser Ding ist wieder kaputt gegangen...

Okay, das liegt daran, dass Kinder im obigen Fall verschachtelte Arrays sein können, wir müssen sie flach machen.

Aber das reicht nicht, pfui, unser createDom erkennt nur entweder Strings oder Elemente, keine Ganzzahlen, also müssen wir die Zahlen aneinanderreihen.

function buildDom(fiber: Fiber, fiberIsFragment: boolean) {
    if(fiber.dom) return
    if(fiberIsFragment) return
    fiber.dom = createDom(fiber.vDom)
}

function performUnitOfWork(nextUnitOfWork: Fiber | null): Fiber | null {
    if(!nextUnitOfWork) {
        return null
    }
    const fiber = nextUnitOfWork
    const fiberIsFragment = isFragment(fiber.vDom)

    reconcile(fiber)

    buildDom(fiber, fiberIsFragment);

    if (fiber.child) {
        return fiber.child
    }
    let nextFiber: Fiber | null = fiber
    while (nextFiber) {
        if (nextFiber.sibling) {
          return nextFiber.sibling
        }
        nextFiber = nextFiber.parent
    }
    return null
}

Okay, jetzt funktioniert alles – irgendwie.

Wenn Sie auf die Schaltfläche „Rendern“ klicken, wird die Liste aktualisiert – das alte Element bleibt jedoch weiterhin erhalten. Wir müssen das alte Element löschen.

Entfernen

Wir wiederholen hier die Regel: Wenn für jede neue Faser kein Kind oder Geschwister vorhanden ist, die alte Faser jedoch ein Kind oder Geschwister hat, entfernen wir rekursiv das alte Kind oder Geschwister.

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent?.dom?.appendChild(fiber.dom)
            fiber.committed = true
        }

        if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) {
            let parent = fiber.parent
            // find the first parent that is not a fragment
            while(parent && isFragment(parent.vDom)) {
                // the root element is guaranteed to not be a fragment has has a non-fragment parent
                parent = parent.parent!
            }
            parent.dom?.appendChild(fiber.dom!)
            fiber.committed = true
        }

        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    oldFiber = wip
    wip = null
}

Wenn Sie keine rekursive Entfernung durchführen, bleiben einige alte Elemente hängen, wenn mehrere Dinge gelöscht werden müssen. Sie können zu
wechseln

function commit() {
    function commitToParent(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent?.dom?.appendChild(fiber.dom)
            fiber.committed = true
        }

        if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom) && !fiber.committed) {
            let parent = fiber.parent
            // find the first parent that is not a fragment
            while(parent && isFragment(parent.vDom)) {
                // the root element is guaranteed to not be a fragment has has a non-fragment parent
                parent = parent.parent!
            }
            parent.dom?.appendChild(fiber.dom!)
            fiber.committed = true
        }

        commitToParent(fiber.child)
        commitToParent(fiber.sibling)
    }
    commitToParent(wip)
    wipParent?.appendChild(wip!.dom!)
    wip!.committed = true
    oldFiber = wip
    wip = null
}

Zur Referenz.

Zusammenfassung

Dies ist ein schwieriges Kapitel – aber ehrlich gesagt ziemlich traditionelles Codieren. Bisher haben Sie jedoch verstanden, wie React von unten bis oben funktioniert.

Eigentlich kann es schon jetzt funktionieren – wir können jedes Mal, wenn wir die Requisiten ändern, manuell ein erneutes Rendern auslösen. Solch frustrierende Handarbeit ist jedoch nicht das, was wir wollen. Wir möchten, dass die Reaktion automatisch erfolgt. Daher werden wir im nächsten Kapitel über Hooks sprechen.

Das obige ist der detaillierte Inhalt vonErstellen Sie ein Tiny React Chpdating 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