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.
D'accord, avant de plonger dans les accroches, nous avons besoin de résumer un peu le dernier chapitre - encore quelque chose à corriger, mais le dernier chapitre était de trop, alors, eh bien, le voici.
Voici quelques choses mineures - pas entièrement des bugs, mais il vaut mieux les corriger.
En javascript, deux fonctions ne sont égales que si elles sont identiques, restent inégales même si elles ont la même procédure, c'est-à-dire
const a = () => 1; const b = () => 1; a === b; // false
Donc, en ce qui concerne la comparaison vDOM, nous devrions ignorer la comparaison des fonctions. Voici le correctif,
for (let i = 0; i < aKeys.length; i++) { const key = aKeys[i] if (key === 'key') continue if (aProps[key] instanceof Function && bProps[key] instanceof Function) 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] instanceof Function && bProps[key] instanceof Function) continue if (aProps[key] !== bProps[key]) return false }
Le style doit être traité comme une propriété spéciale attribuée à l'élément avec la propriété .style. Voici le correctif,
export type VDomAttributes = { key?: string | number style?: object [_: string]: unknown | undefined } export function createDom(vDom: VDomNode): HTMLElement | Text { if (isElement(vDom)) { const element = document.createElement(vDom.tag) Object.entries(vDom.props ?? {}).forEach(([name, value]) => { if (value === undefined) return if (name === 'key') return if (name === 'style') { Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => { element.style[styleName as any] = styleValue as any }) 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 { return document.createTextNode(vDom) } }
Maintenant que ces corrections secondaires sont terminées, passons au sujet principal de ce chapitre : les crochets.
Nous avons précédemment appelé explicitement render(vDom, app!), ce qui nécessite la création de vDOM par l'utilisateur, voici une meilleure façon de le faire.
import { mount, useState, type FuncComponent } from "./runtime"; import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom"; const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => { const [cnt, setCnt] = useState(0) return <div> <button onClick={() => setCnt(cnt() + 1)}>Click me</button> <p>Count: {cnt()}</p> </div> } const app = document.getElementById('app') mount(App, {}, [], app!)
let reRender: () => void = () => {} export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) { reRender = () => { const vDom = app(props, children) as unknown as VDomNode render(vDom, parent) } reRender() }
Ça a l'air mieux plus ou moins. Passons maintenant au sujet principal de ce chapitre : les crochets.
D'accord, passons au crochet. Le premier hook que nous allons implémenter est useState. C'est un hook qui nous permet de gérer l'état d'un composant. Nous pouvons avoir la signature suivante pour useState,
Notez que notre implémentation est légèrement différente du React original. Nous allons renvoyer une fonction getter et setter, au lieu de renvoyer l'état directement.
function useState<T>(initialValue: T): [() => T, (newValue: T) => void] { // implementation }
Alors, où allons-nous fixer la valeur ? Si nous la cachons simplement dans la fermeture elle-même, la valeur sera perdue lorsque le composant sera restitué. Si vous insistez pour le faire, vous devez accéder à l'espace de la fonction externe, ce qui n'est pas possible en javascript.
Notre façon de faire est donc de le stocker, vous l'aurez deviné, dans les fibres. Alors, ajoutons un champ à la fibre.
interface Fiber { parent: Fiber | null sibling: Fiber | null child: Fiber | null vDom: VDomNode dom: HTMLElement | Text | null alternate: Fiber | null committed: boolean hooks?: { state: unknown[] }, hookIndex?: { state: number } }
Et nous montons uniquement des crochets sur la fibre racine, nous pouvons donc ajouter la ligne suivante à la fonction de montage.
export function render(vDom: VDomNode, parent: HTMLElement) { wip = { parent: null, sibling: null, child: null, vDom: vDom, dom: null, committed: false, alternate: oldFiber, hooks: oldFiber?.hooks ?? { state: [] }, hookIndex: { state: 0 } } wipParent = parent nextUnitOfWork = wip }
L'index de crochet sera utilisé plus tard. Désormais, l'index du hook est réinitialisé à chaque fois que le composant est restitué, mais les anciens hooks sont conservés.
Veuillez noter que, lors du rendu du composant vDOM, seule l'ancienne fibre est accessible, nous ne pouvons donc manipuler que cette variable. Cependant, il est nul au tout début, alors créons un mannequin.
const a = () => 1; const b = () => 1; a === b; // false
Maintenant, nous aurons un gros temps de réflexion - puisque l'ordre de chaque appel de hook est fixe (vous ne pouvez pas utiliser de hooks dans une boucle ou une condition, règle de base de React, vous savez pourquoi c'est maintenant), nous pouvons donc l'utiliser en toute sécurité utilisez hookIndex pour accéder au hook.
for (let i = 0; i < aKeys.length; i++) { const key = aKeys[i] if (key === 'key') continue if (aProps[key] instanceof Function && bProps[key] instanceof Function) 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] instanceof Function && bProps[key] instanceof Function) continue if (aProps[key] !== bProps[key]) return false }
Eh bien, essayons,
export type VDomAttributes = { key?: string | number style?: object [_: string]: unknown | undefined } export function createDom(vDom: VDomNode): HTMLElement | Text { if (isElement(vDom)) { const element = document.createElement(vDom.tag) Object.entries(vDom.props ?? {}).forEach(([name, value]) => { if (value === undefined) return if (name === 'key') return if (name === 'style') { Object.entries(value as Record<string, unknown>).forEach(([styleName, styleValue]) => { element.style[styleName as any] = styleValue as any }) 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 { return document.createTextNode(vDom) } }
Cela fonctionne en quelque sorte : le nombre est passé de zéro à un, mais il n'augmente pas davantage.
Eh bien... étrange, n'est-ce pas ? Voyons ce qui se passe, c'est l'heure du débogage.
import { mount, useState, type FuncComponent } from "./runtime"; import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom"; const App: FuncComponent = (props: VDomAttributes, __: VDomNode[]) => { const [cnt, setCnt] = useState(0) return <div> <button onClick={() => setCnt(cnt() + 1)}>Click me</button> <p>Count: {cnt()}</p> </div> } const app = document.getElementById('app') mount(App, {}, [], app!)
Vous verrez ça, il enregistre toujours 1. Mais la page web nous dit que c'est 1, donc ça devrait être 2. Que se passe-t-il ?
Pour les types natifs, javascript passe par valeur, donc la valeur est copiée et non référencée. Dans le composant de classe React, vous devez disposer d'un objet d'état pour résoudre les problèmes. En composant fonctionnel, le React s'adresse avec, fermeture. Mais si nous devions utiliser cette dernière solution, cela nécessiterait un grand changement dans notre code. Donc, un moyen simple d'obtenir un laissez-passer consiste à utiliser la fonction pour obtenir l'état, afin que la fonction renvoie toujours le dernier état.
let reRender: () => void = () => {} export function mount(app: FuncComponent, props: VDomAttributes, children: VDomNode[], parent: HTMLElement) { reRender = () => { const vDom = app(props, children) as unknown as VDomNode render(vDom, parent) } reRender() }
Et maintenant, nous l’avons ! Ça marche! Nous avons créé le hook useState pour notre petit React.
D'accord, vous pensez peut-être que ce chapitre est trop court - les hooks sont importants pour réagir, alors pourquoi avons-nous uniquement implémenté useState ?
Premièrement, de nombreux hooks ne sont que des variantes de useState. Un tel hook n'a aucun rapport avec le composant qu'il s'appelle, par exemple useMemo. De telles choses ne sont que des travaux insignifiants et nous n'avons pas de temps à perdre.
Mais la deuxième raison, la plus importante, est que, pour des hooks comme useEffect, dans notre cadre actuel basé sur la mise à jour racine, ils sont presque impossibles à faire. Lorsqu'une fibre est démontée, vous ne pouvez pas signaler, car nous récupérons uniquement le vDOM global et mettons à jour l'ensemble du vDOM, alors que, dans le vrai React, ce n'est pas comme ça.
Dans le vrai React, les composants fonctionnels sont mis à jour par le composant parent, afin que le composant parent puisse signaler au composant enfant de démonter. Mais dans notre cas, nous mettons à jour uniquement le composant racine, nous ne pouvons donc pas signaler au composant enfant de démonter.
Cependant, le petit projet actuel a essentiellement démontré comment fonctionne React, et j'espère que cela vous sera utile pour mieux comprendre le framework React.
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!