Tutorial ini berdasarkan tutorial ini, tetapi dengan JSX, skrip taip dan pendekatan yang lebih mudah untuk dilaksanakan. Anda boleh menyemak nota dan kod pada repo GitHub saya.
Sekarang mari kita bincangkan tentang kereaktifan.
Kita perlu simpan fiber lama supaya kita boleh bandingkan dengan fiber baru. Kita boleh melakukan ini dengan menambah medan pada gentian. Kami juga memerlukan bidang yang komited- yang akan berguna nanti.
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 }
Kemudian kami menetapkan keadaan komited di sini,
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 }
Kita juga perlu menyelamatkan pokok gentian lama.
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 }
Kini, kita perlu membandingkan gentian lama dengan gentian baharu semasa lelaran. Ini dipanggil proses perdamaian.
Kita perlu membandingkan serat lama dengan serat baru. Kami mula-mula meletakkan serat lama dalam kerja awal.
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 }
Kemudian kami memisahkan penciptaan gentian baharu kepada fungsi baharu.
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 }
Walau bagaimanapun, kita perlu memasang gentian lama pada gentian baharu.
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++ } } }
Kini kami mempunyai gentian lama yang dipasang pada gentian baharu. Tetapi kami tidak mempunyai apa-apa untuk mencetuskan pemaparan semula- buat masa ini, kami mencetuskannya secara manual dengan menambahkan butang. Memandangkan kami belum mempunyai keadaan lagi, kami menggunakan prop untuk memutasi 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)
Sekarang jika anda mengklik butang render, hasil yang diberikan akan berulang sekali, kerana, baik, semua logik semasa kami hanya meletakkan vDOM yang diberikan ke dalam dokumen.
Jika anda menambah console.log dalam fungsi komit, anda boleh melihat gentian alternatif dicetak keluar.
Sekarang kita perlu menentukan cara kita mengendalikan gentian lama dan gentian baharu, dan mengubah DOM berdasarkan maklumat. Peraturannya adalah seperti berikut.
Untuk setiap gentian baharu,
Agak keliru? Baik, saya hanya akan menunjukkan kod. Kami memadamkan penciptaan DOM lama dahulu. Kemudian gunakan peraturan di atas.
Peraturan pertama, jika ada serat lama, kita bandingkan kandungan serat lama dengan serat baru. Jika ia berbeza, kami menggantikan nod DOM lama dengan nod DOM baharu, atau kami menyalin nod DOM lama ke nod DOM baharu.
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 } }
Kemudian saya membuat beberapa refactor kecil,
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 }
Kini, apabila bercakap tentang komitmen, kami mempunyai medan alternatif tambahan untuk membandingkan gentian lama dengan gentian baharu.
Ini ialah fungsi komit asal,
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 }
Kami akan menukar sedikit nama. Nama lama cuma salah (maafkan saya).
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 }
Jadi apa yang perlu kita lakukan? Logik lama kami hanya menambah, jadi kami mengeluarkannya,
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 }
Kami perlu menangguhkan pembinaan DOM sehingga fasa komit, untuk memberikan lebih fleksibiliti.
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 }
Mengikut peraturan pertama dan kedua, kami memfaktorkannya semula ke dalam kod berikut,
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++ } } }
Sila sentiasa ingat bahawa dalam javascript, semua nilai adalah rujukan. Jika kita mempunyai fiber.dom = fiber.alternate.dom, maka fiber.dom dan fiber.alternate.dom akan menghala ke objek yang sama. Jika kita menukar fiber.dom, fiber.alternate.dom juga akan berubah, begitu juga sebaliknya. Itulah sebabnya apabila menggantikan, kami hanya menggunakan fiber.alternate.dom?.replaceWith(fiber.dom). Ini akan menggantikan DOM lama dengan DOM baharu. Walaupun ibu bapa terdahulu, jika disalin, mempunyai fiber.alternate.dom untuk DOM mereka, DOM mereka juga akan diganti.
Walau bagaimanapun, kami belum lagi mengendalikan pemadaman.
Baiklah, kod sebelumnya mengandungi beberapa pepijat yang saya nampak semasa saya menulis jsx yang lebih kompleks, jadi, sebelum melaksanakan pemadaman, mari kita betulkan.
Sebelum ini terdapat pepijat- kami tidak boleh menghantar senarai kepada prop, mari gunakan peluang ini untuk memperbaikinya.
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)
Kemudian anda hanya membetulkan jenis perkara- hanya satu ralat untuk saya, jadi, sila lakukan sendiri.
Walau bagaimanapun, jika kita mempunyai kod berikut,
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 } }
Perkara kami rosak lagi...
Baiklah, ini kerana kanak-kanak boleh disusun tatasusunan dalam kes di atas, kita perlu meratakannya.
Tetapi itu tidak mencukupi, oh, createDom kami hanya mengenali sama ada rentetan atau elemen, bukan integer, jadi, kami perlu Rentekan nombor.
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 }
Baiklah, semuanya berfungsi sekarang.
Jika anda menekan butang render, senarai dikemas kini- tetapi elemen lama masih kekal. Kita perlu memadamkan elemen lama.
Kami menyatakan semula peraturan di sini- untuk mana-mana gentian baharu, jika ia tidak mempunyai anak atau adik beradik, tetapi gentian lamanya mempunyai anak atau adik beradik, kami mengeluarkan anak atau adik beradik yang lama secara rekursif.
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 }
Jika anda tidak mengeluarkan rekursif, beberapa elemen lama akan berjuntai apabila anda mempunyai beberapa perkara yang memerlukan pemadaman. Anda boleh bertukar kepada,
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 }
Untuk rujukan.
Ini adalah bab yang sukar- tetapi pengekodan yang agak tradisional, sejujurnya. Walau bagaimanapun, sehingga kini, anda telah memahami cara React berfungsi dari bawah ke atas.
Sebenarnya, perkara sudah boleh berfungsi sekarang- kami boleh mencetuskan pemaparan semula secara manual apabila kami menukar prop. Walau bagaimanapun, kerja manual yang mengecewakan itu bukanlah yang kita mahukan. Kami mahu kereaktifan menjadi automatik. Jadi, kita akan bercakap tentang cangkuk dalam bab seterusnya.
Atas ialah kandungan terperinci Bina Tiny React Chpdating vDOM. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!