Maison > interface Web > js tutoriel > le corps du texte

Créer un petit vDOM React Chpdating

Linda Hamilton
Libérer: 2024-10-20 18:31:30
original
232 Les gens l'ont consulté

Build a Tiny React Chpdating vDOM

Ce tutoriel est basé sur ce tutoriel, mais avec JSX, dactylographié et une approche plus simple à mettre en œuvre. Vous pouvez consulter les notes et le code sur mon dépôt GitHub.

Parlons maintenant de la réactivité.

Sauvez l’ancienne fibre

Nous devons conserver l’ancienne fibre afin de pouvoir la comparer avec la nouvelle fibre. Nous pouvons le faire en ajoutant un champ à la fibre. Il nous faut aussi un terrain engagé- qui nous sera utile plus tard.

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
}
Copier après la connexion
Copier après la connexion

Ensuite, nous définissons ici l'état engagé,

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
}
Copier après la connexion
Copier après la connexion

Nous devons également sauver le vieil arbre à fibres.

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
}
Copier après la connexion
Copier après la connexion

Maintenant, nous devons comparer l'ancienne fibre avec la nouvelle fibre lors de l'itération. C'est ce qu'on appelle le processus de réconciliation.

Réconciliation

Nous devons comparer l’ancienne fibre avec la nouvelle fibre. Nous mettons d'abord l'ancienne fibre dans le travail initial.

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
}
Copier après la connexion
Copier après la connexion

Ensuite, nous séparons la création de la nouvelle fibre en une nouvelle fonction.

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
}
Copier après la connexion
Copier après la connexion

Cependant, nous devons monter l'ancienne fibre sur la nouvelle.

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++
        }
    }
}
Copier après la connexion
Copier après la connexion

Maintenant, nous avons l'ancienne fibre montée sur la nouvelle fibre. Mais nous n'avons rien pour déclencher le nouveau rendu. Pour l'instant, nous le déclenchons manuellement en ajoutant un bouton. Comme nous n'avons pas encore d'état, nous utilisons des accessoires pour muter le 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)
Copier après la connexion
Copier après la connexion

Maintenant, si vous cliquez sur le bouton de rendu, le résultat rendu se répétera une fois, puisque, eh bien, toute notre logique actuelle consiste simplement à mettre le vDOM rendu dans le document.

Si vous ajoutez un console.log dans la fonction de validation, vous pouvez voir la fibre alternative en cours d'impression.

Nous devons maintenant définir comment nous gérons l'ancienne fibre et la nouvelle fibre, et muter le DOM en fonction des informations. Les règles sont les suivantes.

Pour chaque nouvelle fibre,

  • S'il y avait une ancienne fibre, on compare le contenu de l'ancienne fibre avec la nouvelle fibre, si ils sont différents, on remplace l'ancien nœud DOM par le nouveau nœud DOM, ou bien on copie l'ancien nœud DOM dans le nouveau nœud DOM. Veuillez noter que lorsque deux vDOM sont égaux, nous entendons leurs balises et toutes leurs propriétés sont égales. Leurs enfants peuvent être différents.
  • S'il n'y a pas d'ancienne fibre, nous créons un nouveau nœud DOM et l'ajoutons au parent.
  • Si, pour la nouvelle fibre, elle n'a pas d'enfant ou de frère, mais que son ancienne fibre a un enfant ou un frère, on supprime récursivement l'ancien enfant ou frère.

Un peu confus ? Eh bien, je vais juste montrer le code. Nous supprimons d'abord l'ancienne création DOM. Appliquez ensuite les règles ci-dessus.

La première règle, s'il y a une ancienne fibre, on compare la teneur de l'ancienne fibre avec la nouvelle fibre. S'ils sont différents, on remplace l'ancien nœud DOM par le nouveau nœud DOM, ou bien on copie l'ancien nœud DOM vers le nouveau nœud DOM.

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
    }
}
Copier après la connexion
Copier après la connexion

Ensuite, j'ai fait un petit refactor,

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
}
Copier après la connexion
Copier après la connexion

Maintenant, lorsqu'il s'agit de s'engager, nous disposons d'un champ alternatif supplémentaire pour comparer l'ancienne fibre avec la nouvelle fibre.

Il s'agit de la fonction de validation originale,

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
}
Copier après la connexion
Copier après la connexion

Nous allons changer un peu le nom. L'ancien nom est tout simplement faux (j'en suis désolé).

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
}
Copier après la connexion
Copier après la connexion

Ajout, copie et remplacement

Alors que devons-nous faire ? Notre ancienne logique ne fait qu'ajouter, donc nous extrayons cela,

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
}
Copier après la connexion
Copier après la connexion

Nous devons retarder la construction du DOM jusqu'à la phase de commit, pour offrir plus de flexibilité.

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
}
Copier après la connexion
Copier après la connexion

En suivant la première et la deuxième règle, nous les refactorisons dans le code suivant,

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++
        }
    }
}
Copier après la connexion
Copier après la connexion

Veuillez toujours garder à l'esprit qu'en javascript, toutes les valeurs sont des références. Si nous avons fibre.dom = fibre.alternate.dom, alors fibre.dom et fibre.alternate.dom pointeront vers le même objet. Si nous changeons fibre.dom, fibre.alternate.dom changera également, et vice versa. C'est pourquoi lors du remplacement, nous avons simplement utilisé fibre.alternate.dom?.replaceWith(fiber.dom). Cela remplacera l'ancien DOM par le nouveau DOM. Alors que les parents précédents, s'ils sont copiés, ont le fibre.alternate.dom pour leur DOM, leur DOM sera également remplacé.

Cependant, nous n'avions pas encore géré la suppression.

Quelques mésaventures

D'accord, le code précédent contient quelques bugs que j'ai repérés alors que j'écrivais du jsx plus complexe, donc, avant d'implémenter la suppression, corrigeons-les.

Il y avait un bug auparavant - nous ne pouvons pas transmettre la liste aux accessoires, profitons de cette opportunité pour le corriger.

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)
Copier après la connexion
Copier après la connexion

Ensuite, corrigez simplement les éléments de type - une seule erreur pour moi, alors faites-le vous-même s'il vous plaît.

Cependant, si nous avons le code suivant,

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
    }
}
Copier après la connexion
Copier après la connexion

Notre truc s'est encore cassé...

D'accord, c'est parce que les enfants peuvent être des tableaux imbriqués dans le cas ci-dessus, nous devons les aplatir.

Mais cela ne suffit pas, pouah, notre createDom ne reconnaît que les chaînes ou les éléments, pas les entiers, nous devons donc enchaîner les nombres.

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
}
Copier après la connexion

D'accord, les choses fonctionnent maintenant, en quelque sorte.

Si vous appuyez sur le bouton de rendu, la liste est mise à jour, mais l'ancien élément reste toujours. Nous devons supprimer l'ancien élément.

Retirer

Nous reformulons ici la règle : pour toute nouvelle fibre, si elle n'a pas d'enfant ou de frère ou de sœur, mais que son ancienne fibre a un enfant ou un frère ou une sœur, nous supprimons récursivement l'ancien enfant ou frère ou sœur.

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
}
Copier après la connexion

Si vous n'effectuez pas de suppression récursive, certains anciens éléments resteront en suspens lorsque plusieurs éléments doivent être supprimés. Vous pouvez changer pour,

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
}
Copier après la connexion

Pour référence.

Résumé

C'est un chapitre difficile, mais un codage assez traditionnel, pour être honnête. Cependant, jusqu'à présent, vous avez compris comment React fonctionne de bas en haut.

En fait, les choses peuvent déjà fonctionner maintenant – nous pouvons déclencher manuellement un nouveau rendu chaque fois que nous changeons les accessoires. Cependant, un travail manuel aussi frustrant n’est pas ce que nous souhaitons. Nous souhaitons que la réactivité soit automatique. Nous parlerons donc des crochets dans le prochain chapitre.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal