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.
Le rendu de vDOM est simple, trop simple. Vous devez connaître les API Web natives suivantes.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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!