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é.
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 }
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 }
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 }
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.
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 }
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 }
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++ } } }
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)
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,
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 } }
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 }
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 }
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 }
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 }
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 }
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++ } } }
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.
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)
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 } }
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 }
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.
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 }
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 }
Pour référence.
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!