Maison  >  Article  >  interface Web  >  Créer un petit vDOM React Chendering

Créer un petit vDOM React Chendering

Patricia Arquette
Patricia Arquetteoriginal
2024-10-20 18:28:30758parcourir

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

Dans cette partie, nous allons rendre le vDOM au DOM réel. De plus, nous présenterons également l'arbre à fibres, qui est une structure de base dans React.

Rendu du vDOM

Le rendu de vDOM est simple, trop simple. Vous devez connaître les API Web natives suivantes.

  • document.createElement(tagName: string): HTMLElement Crée un élément DOM réel.
  • document.createTextNode(text: string): Text Crée un nœud de texte.
  • .appendChild(child: Node): void Ajoute un nœud enfant au nœud parent. Une méthode sur HTMLElement
  • .removeChild(child: Node) : void Supprime un nœud enfant du nœud parent. Une méthode sur HTMLElement
  • .replaceChild(newChild : Node, oldChild : Node) : void Remplace un nœud enfant par un nouveau nœud enfant. Une méthode sur HTMLElement
  • .replaceWith(...nodes: Node[]): void Remplace un nœud par de nouveaux nœuds. Une méthode sur Node
  • .remove() : void Supprime un nœud du document. Une méthode sur Node
  • .insertBefore(newChild: Node, refChild: Node): void Insère un nouveau nœud enfant avant un nœud enfant de référence. Une méthode sur HTMLElement
  • .setAttribute(name : string, value : string) : void Définit un attribut sur un élément. Une méthode sur HTMLElement.
  • .removeAttribute(name: string): void Supprime un attribut d'un élément. Une méthode sur HTMLElement.
  • .addEventListener(type : string, écouteur : Fonction) : void Ajoute un écouteur d'événement à un élément. Une méthode sur HTMLElement.
  • .removeEventListener(type : string, écouteur : Fonction) : void Supprime un écouteur d'événement d'un élément. Une méthode sur HTMLElement.
  • .dispatchEvent(event: Event) : void Distribue un événement sur un élément. Une méthode sur HTMLElement.

Woa, un peu trop, non ? Mais tout ce que vous avez à faire est de refléter la création du vDOM dans le DOM réel. Voici un exemple simple.

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)
        }
    }
}

Nous avons enregistré des propriétés commençant par on en tant qu'écouteurs d'événements, c'est une pratique courante dans React. De plus, nous avons ignoré la propriété key, qui est utilisée pour la réconciliation, pas pour le rendu.

D'accord, donc le rendu est terminé et ce chapitre se termine... ? Non.

Rendu des temps d'inactivité

En vrai React, le processus de rendu est un peu plus compliqué. Pour être plus précis, il utilisera requestIdleCallback pour effectuer en premier les tâches les plus urgentes, réduisant ainsi sa propre priorité.

Veuillez noter que requestIdleCallback n'est pas pris en charge sur Safari, ni sur MacOS ni sur iOS (ingénieurs Apple, s'il vous plaît, pourquoi ? Au moins, ils y travaillent, en 2024). Si vous êtes sur un Mac, utilisez Chrome ou remplacez-le par un simple setTimeout. Dans Real React, il utilise un planificateur pour gérer cela, mais l'idée de base est la même.

Pour ce faire, nous devons connaître les API Web natives suivantes.

  • requestIdleCallback(callback: Function): void Demande qu'un rappel soit appelé lorsque le navigateur est inactif. Le rappel recevra un objet IdleDeadline. Le rappel aura un argument date limite, qui est un objet avec les propriétés suivantes.
    • timeRemaining() : nombre Renvoie le temps restant en millisecondes avant que le navigateur ne soit plus inactif. Nous devrions donc terminer notre travail avant la fin du temps imparti.

Nous devons donc diviser notre rendu en morceaux et utiliser requestIdleCallback pour le gérer. Un moyen simple serait de restituer simplement un nœud à la fois. C'est facile - mais ne vous précipitez pas - sinon vous perdrez beaucoup de temps, car nous avons également besoin d'autres travaux à effectuer lors du rendu.

Mais nous pouvons avoir le code suivant comme cadre de base pour ce que nous allons faire.

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)
        }
    }
}

Si vous remplissez maintenant // TODO avec le rendu du vDOM et renvoyez le prochain nœud vDOM à rendre, vous pouvez avoir un simple rendu de temps d'inactivité. Mais ne vous précipitez pas, nous avons besoin de plus de travail.

Arbre à fibres

Dans le prochain chapitre, nous implémenterons la réactivité, et la réconciliation est plutôt compliquée - nous déplaçons donc du contenu dans cette partie, qui est l'arbre à fibres.

L'arbre à fibres n'est qu'une structure de données spéciale. Lorsque React gère les modifications, il effectue le processus suivant.

  1. Quelque chose, peut-être un utilisateur, ou un rendu initial, déclenche un changement.
  2. React crée une nouvelle arborescence vDOM.
  3. React calcule le nouvel arbre à fibres.
  4. React calcule la différence entre l'ancien arbre à fibres et le nouveau arbre à fibres.
  5. React applique la différence au DOM réel.

Vous pouvez le constater, l'arbre à fibres est essentiel pour React.

L'arbre à fibres, un peu différent de l'arbre traditionnel, possède trois types de relations entre les nœuds.

  • enfant de : Un nœud est un enfant d'un autre nœud. Veuillez noter que, dans l'arbre à fibres, chaque nœud ne peut avoir qu'un seul enfant. La structure arborescente traditionnelle est représentée par un enfant avec de nombreux frères et sœurs.
  • frère de : Un nœud est le frère d'un autre nœud.
  • parent de : Un nœud est le parent d’un autre nœud. Différent de l'enfant de, de nombreux nœuds peuvent partager le même parent. Vous pouvez considérer le nœud parent dans l'arbre à fibres comme un mauvais parent, qui ne se soucie que du premier enfant, mais qui est toujours, en fait, le parent de nombreux enfants.

Par exemple, pour le DOM suivant,

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')
}

On peut le représenter comme un arbre.

<div>
    <p></p>
    <div>
        <h1></h1>
        <h2></h2>
    </div>
</div>

p est un enfant du div racine, mais le div secondaire n'est pas un enfant du div racine, mais un frère de p. h1 et h2 sont des enfants de la division secondaire.

En ce qui concerne le rendu, l'ordre est principalement axé sur la profondeur, mais un peu différent - donc fondamentalement, il suit ces règles. Pour chaque nœud, il passe par les étapes suivantes.

  1. Si ce nœud a un enfant non traité, traitez l'enfant.
  2. Si ce nœud a un frère, traitez ce frère. Répétez jusqu'à ce que tous les frères et sœurs soient traités.
  3. Marquer ce nœud comme traité.
  4. Traitement de son parent.

Maintenant, implémentons cela. Mais d’abord, nous devons déclencher le processus de rendu. C'est simple : il suffit de définir nextUnitOfWork à la racine de l'arborescence de fibres.

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)
        }
    }
}

Après avoir déclenché le rendu, le navigateur appellera performUnitOfWork, c'est là que nous effectuons le travail.

La première est que nous devons créer de véritables éléments DOM. Nous pouvons le faire en créant un nouvel élément DOM et en l'ajoutant à l'élément DOM parent.

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>

C'est la première partie du travail. Nous devons maintenant construire la fibre dérivée de celle actuelle.

div
├── p
└── div
    ├── h1
    └── h2

Nous avons maintenant un arbre à fibres construit pour le nœud actuel. Suivons maintenant nos règles pour traiter l'arbre à fibres.

export function render(vDom: VDomNode, parent: HTMLElement) {
    nextUnitOfWork = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: parent
    }
}

Maintenant, nous pouvons restituer le vDOM, le voici. Veuillez noter que le dactylographie est stupide ici car il ne peut pas déterminer le type de notre DOM virtuel, nous avons besoin d'un vilain contournement ici.

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')
    }
}

Maintenant, votre vDOM est rendu dans le DOM réel. Félicitations! Vous avez fait un excellent travail. Mais nous n’avons pas encore fini.

Engagement cumulatif

Il y aura un problème avec l'implémentation actuelle - si nous avons trop de nœuds qui ralentissent l'ensemble du processus, l'utilisateur verra comment le rendu est effectué. Bien sûr, cela ne divulguera pas de secrets commerciaux ou autre, mais ce n'est pas une bonne expérience. Nous préférons cacher la création dom derrière le rideau, et la soumettre d'un seul coup.

La solution est simple : au lieu de s'engager directement dans le document, nous créons un élément sans l'ajouter au document, et lorsque nous avons terminé, nous l'ajoutons au document. C'est ce qu'on appelle la validation cumulative.

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')
}

Maintenant, nous supprimons l'appendChild de performUnitOfWork, c'est-à-dire la partie suivante,

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++
    }
}

Maintenant, si nous finissons tout le travail, nous avons toutes les fibres correctement construites avec leur DOM, mais elles ne sont pas ajoutées au document. Lorsqu'un tel événement est distribué, nous appelons une fonction de validation, qui ajoutera le DOM au document.

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

Maintenant, la fonction de validation est simple : ajoutez simplement tous les enfants du DOM de manière récursive au wip, puis validez le wip dans le 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!)

Vous pouvez tester cela en ajoutant un délai d'attente à la fonction commitChildren. auparavant, le rendu se faisait étape par étape, mais maintenant il se fait en une seule fois.

Composants imbriqués

Vous pouvez essayer des fonctions imbriquées, comme celles-ci,

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)
        }
    }
}

Mais cela ne fonctionnera pas, car lors de l'analyse du JSX, la balise n'est que le nom de l'étiquette. Bien sûr, pour les éléments natifs, il s’agit simplement d’une chaîne, mais pour les composants, il s’agit d’une fonction. Ainsi, lors du processus de conversion de JSX en vDOM, nous devons vérifier si la balise est une fonction, et si c'est le cas, l'appeler.

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')
}

Désormais, des accessoires et des enfants sont nécessaires pour chaque composant. Dans le vrai React, ils ont ajouté un champ supplémentaire à vérifier - vous pouvez imaginer, simplement en remplaçant les fonctions par des classes, vous avez donc des champs supplémentaires - puis vous fournissez une nouvelle fonction pour créer des objets, un modèle d'usine typique - mais nous prenons un paresseux ici.

<div>
    <p></p>
    <div>
        <h1></h1>
        <h2></h2>
    </div>
</div>

Veuillez noter que dans le vrai React, l'appel du composant de fonction est retardé jusqu'à l'étape de construction de la fibre. Néanmoins, nous l'avons fait par commodité, et cela ne nuit pas vraiment au but de cette série.

Fragment

Cependant, ce n’est toujours pas suffisant. Auparavant, nous traitions simplement le fragment comme un div, ce qui n'est pas correct. Mais si vous remplacez simplement cela par un fragment de document, cela ne fonctionnera pas. La raison en est que les fragments sont un conteneur à usage unique, ce qui conduit à un comportement étrange, comme si vous ne pouviez pas en extraire des éléments réels, ni les imbriquer, ni beaucoup de choses étranges (en réalité, pourquoi cela a-t-il simplement gagné ?) ça ne marche pas plus simple...). Alors putain, il faut qu'on déterre cette merde.

La solution est donc que nous ne créons pas de DOM pour un fragment - nous trouvons le bon parent pour ajouter le DOM.

Nous avons besoin,

div
├── p
└── div
    ├── h1
    └── h2

Et changez le rendu,

export function render(vDom: VDomNode, parent: HTMLElement) {
    nextUnitOfWork = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: parent
    }
}

Maintenant, le fragment est correctement traité.

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!

Déclaration:
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