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.
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.
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,
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 }
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.
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.
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.
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!