Bina Tiny React Chpdating vDOM

Linda Hamilton
Lepaskan: 2024-10-20 18:31:30
asal
141 orang telah melayarinya

Build a Tiny React Chpdating vDOM

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.

Simpan Fiber Lama

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

Kini, kita perlu membandingkan gentian lama dengan gentian baharu semasa lelaran. Ini dipanggil proses perdamaian.

Penyesuaian

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

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++
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

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)
Salin selepas log masuk
Salin selepas log masuk

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,

  • Jika ada gentian lama, kita bandingkan kandungan gentian lama dengan gentian baru, jika berbeza, kita gantikan nod DOM lama dengan nod DOM baharu, atau kita salin nod DOM lama ke nod DOM baharu. Sila ambil perhatian bahawa, dengan dua vDOM yang sama, kami maksudkan teg mereka dan semua sifat adalah sama. Anak mereka boleh jadi berbeza.
  • Jika tiada gentian lama, kami mencipta nod DOM baharu dan menambahkannya pada induk.
  • Jika, untuk serabut baru, ia tidak mempunyai anak atau adik-beradik, tetapi serabutnya yang lama mempunyai anak atau adik-beradik, kami secara rekursif mengeluarkan anak atau adik-beradik yang lama.

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
    }
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

Melampirkan, Menyalin dan Mengganti

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
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk
Salin selepas log masuk

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++
        }
    }
}
Salin selepas log masuk
Salin selepas log masuk

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.

Beberapa Kemalangan

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)
Salin selepas log masuk
Salin selepas log masuk

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
    }
}
Salin selepas log masuk
Salin selepas log masuk

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
}
Salin selepas log masuk

Baiklah, semuanya berfungsi sekarang.

Jika anda menekan butang render, senarai dikemas kini- tetapi elemen lama masih kekal. Kita perlu memadamkan elemen lama.

Alih keluar

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
}
Salin selepas log masuk

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
}
Salin selepas log masuk

Untuk rujukan.

Ringkasan

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!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Artikel terbaru oleh pengarang
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!