Bina vDOM Tiny React Chendering

Patricia Arquette
Lepaskan: 2024-10-20 18:28:30
asal
662 orang telah melayarinya

Build a Tiny React Chendering 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.

Bahagian ini kami akan menjadikan vDOM kepada DOM sebenar. Selain itu, kami juga akan memperkenalkan pokok gentian, yang merupakan struktur teras dalam React.

Memaparkan vDOM

Merender vDOM adalah, mudah- terlalu mudah. Anda perlu mengetahui API asli web berikut.

  • document.createElement(tagName: string): HTMLElement Mencipta elemen DOM sebenar.
  • document.createTextNode(text: string): Teks Mencipta nod teks.
  • .appendChild(anak: Nod): void Menambahkan nod anak pada nod induk. Kaedah pada HTMLElement
  • .removeChild(child: Node): void Mengalih keluar nod anak daripada nod induk. Kaedah pada HTMLElement
  • .replaceChild(newChild: Node, oldChild: Nod): void Menggantikan nod kanak-kanak dengan nod anak baharu. Kaedah pada HTMLElement
  • .replaceWith(...nodes: Node[]): void Menggantikan nod dengan nod baharu. Kaedah pada Node
  • .remove(): void Mengalih keluar nod daripada dokumen. Kaedah pada Node
  • .insertBefore(newChild: Node, refChild: Node): void Memasukkan nod anak baharu sebelum nod anak rujukan. Kaedah pada HTMLElement
  • .setAttribute(name: string, value: string): void Menetapkan atribut pada elemen. Kaedah pada HTMLElement.
  • .removeAttribute(name: string): void Mengalih keluar atribut daripada elemen. Kaedah pada HTMLElement.
  • .addEventListener(type: string, listener: Function): void Menambah pendengar acara pada elemen. Kaedah pada HTMLElement.
  • .removeEventListener(type: string, listener: Function): void Mengalih keluar pendengar acara daripada elemen. Kaedah pada HTMLElement.
  • .dispatchEvent(event: Event): void Menghantar acara pada elemen. Kaedah pada HTMLElement.

Wah, agak berlebihan, kan? Tetapi apa yang anda perlu lakukan ialah mencerminkan penciptaan vDOM kepada DOM sebenar. Berikut ialah contoh mudah.

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

Kami mendaftarkan hartanah bermula dengan pada sebagai pendengar acara, ini adalah amalan biasa dalam React. Selain itu, kami mengabaikan sifat utama, yang digunakan untuk perdamaian, bukan untuk pemaparan.

Baiklah, jadi rendering selesai dan bab ini tamat...? Tidak.

Penyampaian Masa Terbiar

Secara nyata, proses pemaparan adalah lebih rumit. Untuk lebih spesifik, ia akan menggunakan requestIdleCallback, untuk membuat tugasan yang lebih mendesak dilakukan terlebih dahulu, mengurangkan keutamaannya sendiri.

Sila ambil perhatian bahawa requestIdleCallback tidak disokong pada Safari, pada kedua-dua MacOS dan iOS (Apple Engineers, sila, mengapa? Sekurang-kurangnya mereka sedang mengusahakannya, pada 2024). Jika anda menggunakan Mac, gunakan chrome atau gantikannya dengan setTimeout yang mudah. Dalam tindak balas sebenar, ia menggunakan penjadual untuk mengendalikan perkara ini, tetapi idea asasnya adalah sama.

Untuk berbuat demikian, kita perlu mengetahui API asli web berikut.

  • requestIdleCallback(callback: Function): void Meminta panggilan balik untuk dipanggil apabila penyemak imbas melahu. Panggilan balik akan dihantar objek IdleDeadline. Panggilan balik akan mempunyai hujah tarikh akhir, yang merupakan objek dengan sifat berikut.
    • timeRemaining(): number Mengembalikan masa yang tinggal dalam milisaat sebelum penyemak imbas tidak lagi melahu. Jadi kita harus menyelesaikan kerja kita sebelum masanya tamat.

Jadi, kami perlu membahagikan pemaparan kami dalam beberapa bahagian dan menggunakan requestIdleCallback untuk mengendalikannya. Cara mudah ialah dengan hanya memberikan satu nod pada satu masa. Ianya mudah- tetapi jangan terlalu bersemangat untuk melakukannya- atau anda akan membuang banyak masa, kerana kami juga memerlukan kerja lain untuk dilakukan semasa membuat persembahan.

Tetapi kita boleh mempunyai kod berikut sebagai rangka kerja asas untuk perkara yang akan kita lakukan.

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

Jika anda kini mengisi // TODO dengan rendering vDOM, dan mengembalikan nod vDOM seterusnya untuk diberikan, anda boleh mempunyai pemaparan masa terbiar yang mudah. Tetapi jangan tergesa-gesa- kita memerlukan lebih banyak kerja.

Pokok Gentian

Dalam bab seterusnya, kami akan melaksanakan kereaktifan, dan perdamaian agak rumit- jadi kami memindahkan beberapa kandungan ke bahagian ini, iaitu pokok gentian.

Pokok gentian hanyalah struktur data khas. Apabila bertindak balas mengendalikan perubahan, ia melakukan proses berikut.

  1. Sesuatu, mungkin pengguna, atau pemaparan awal, mencetuskan perubahan.
  2. React mencipta pepohon vDOM baharu.
  3. Bertindak balas mengira pokok gentian baharu.
  4. React mengira perbezaan antara pokok gentian lama dan pokok gentian baharu.
  5. React menggunakan perbezaan pada DOM sebenar.

Anda boleh lihat, pokok gentian adalah penting untuk React.

Pokok gentian, sedikit berbeza daripada pokok tradisional, mempunyai tiga jenis perhubungan antara nod.

  • anak kepada: Nod ialah anak kepada nod lain. Sila ambil perhatian bahawa, dalam pokok gentian, setiap nod hanya boleh mempunyai seorang anak. Struktur pokok tradisional diwakili oleh seorang kanak-kanak yang mempunyai ramai adik-beradik.
  • adik kepada: Nod ialah adik beradik kepada nod lain.
  • induk kepada: Nod ialah induk kepada nod lain. Berbeza daripada anak, banyak nod boleh berkongsi induk yang sama. Anda boleh menganggap nod induk dalam pokok gentian sebagai ibu bapa yang tidak baik, yang hanya mengambil berat tentang anak pertama, tetapi sebenarnya, ibu bapa kepada ramai anak.

Sebagai contoh, untuk DOM berikut,

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

Kita boleh mewakilinya sebagai pokok.

<div>
    <p></p>
    <div>
        <h1></h1>
        <h2></h2>
    </div>
</div>
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

p ialah anak kepada div punca, tetapi div kedua bukanlah anak kepada div punca, tetapi adik beradik kepada p. h1 dan h2 ialah anak kepada div menengah.

Mengenai pemaparan, tertibnya adalah mengutamakan kedalaman, tetapi agak berbeza- jadi pada asasnya, ia mengikut peraturan ini. Untuk setiap nod, ia melalui langkah berikut.

  1. Jika nod ini mempunyai anak yang belum diproses, proseskan anak itu.
  2. Jika nod ini mempunyai adik beradik, proseskan adik beradik itu. Ulang sehingga semua adik beradik diproses.
  3. Tandai nod ini sebagai diproses.
  4. Proses induknya.

Sekarang mari kita laksanakan itu. Tetapi pertama, kita perlu mencetuskan proses pemaparan. Ianya mudah- cuma tetapkan nextUnitOfWork kepada akar pokok gentian.

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

Selepas mencetuskan pemaparan, penyemak imbas akan memanggil performUnitOfWork, di sinilah kami melakukan kerja.

Pertama ialah kita perlu mencipta elemen DOM sebenar. Kita boleh melakukan ini dengan mencipta elemen DOM baharu dan menambahkannya pada elemen DOM induk.

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')
}
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk
<div>
    <p></p>
    <div>
        <h1></h1>
        <h2></h2>
    </div>
</div>
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Ini adalah bahagian pertama kerja. Sekarang kita perlu membina gentian bercabang daripada yang sekarang.

div
├── p
└── div
    ├── h1
    └── h2
Salin selepas log masuk
Salin selepas log masuk

Kini kami mempunyai pokok gentian yang dibina untuk nod semasa. Sekarang mari ikut peraturan kami untuk memproses pokok gentian.

export function render(vDom: VDomNode, parent: HTMLElement) {
    nextUnitOfWork = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: parent
    }
}
Salin selepas log masuk
Salin selepas log masuk

Kini kita boleh memaparkan vDOM, ini dia. Sila ambil perhatian bahawa skrip taip adalah bodoh di sini kerana ia tidak dapat memberitahu jenis DOM maya kami, kami memerlukan pintasan hodoh di sini.

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

Kini vDOM anda dipaparkan kepada DOM sebenar. tahniah! Anda telah melakukan kerja yang hebat. Tetapi kami belum selesai.

Komitmen Terkumpul

Akan terdapat masalah dengan pelaksanaan semasa- jika kita mempunyai terlalu banyak nod yang melambatkan keseluruhan proses, pengguna akan melihat cara pemaparan dilakukan. Sudah tentu, ia tidak akan membocorkan rahsia komersial atau sesuatu, tetapi ia bukan pengalaman yang baik. Kami lebih suka menyembunyikan ciptaan dom di sebalik tirai, menyerahkan semuanya sekaligus.

Penyelesaiannya mudah- daripada terus terikat pada dokumen, kami mencipta elemen tanpa menambahkannya pada dokumen dan apabila kami selesai, kami menambahkannya pada dokumen. Ini dipanggil komitmen kumulatif.

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

Sekarang, kami mengalih keluar appendChild daripada performUnitOfWork, iaitu bahagian berikut,

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

Sekarang jika kita menyelesaikan semua kerja, kita mempunyai semua gentian yang dibina dengan betul dengan DOM mereka, tetapi ia tidak ditambahkan pada dokumen. Apabila acara sedemikian dihantar, kami memanggil fungsi komit, yang akan menambahkan DOM pada dokumen.

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

Sekarang, fungsi commit adalah mudah- cuma tambahkan semua kanak-kanak DOM secara rekursif pada wip, kemudian commit wip ke 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!)
Salin selepas log masuk

Anda boleh menguji ini dengan menambahkan tamat masa pada fungsi commitChildren. sebelum ini, pemaparan dilakukan secara berperingkat, tetapi kini ia dilakukan serentak.

Komponen Bersarang

Anda boleh mencuba fungsi bersarang- seperti berikut,

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

Tetapi ia tidak akan berfungsi, kerana apabila menghuraikan JSX, teg hanyalah nama label. Pasti, untuk elemen asli, ia hanyalah rentetan, tetapi untuk komponen, ia adalah fungsi. Jadi dalam proses menukar JSX kepada vDOM, kita perlu menyemak sama ada teg itu adalah fungsi, dan jika ya, panggilnya.

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

Kini, prop dan kanak-kanak diperlukan untuk setiap komponen. Dalam React sebenar, mereka menambah medan tambahan untuk menyemak- anda boleh bayangkan, hanya dengan menggantikan fungsi dengan kelas, jadi anda mempunyai medan tambahan- kemudian anda menyediakan fungsi baru untuk mencipta objek, corak kilang biasa- tetapi kami mengambil malas kami di sini.

<div>
    <p></p>
    <div>
        <h1></h1>
        <h2></h2>
    </div>
</div>
Salin selepas log masuk
Salin selepas log masuk
Salin selepas log masuk

Sila ambil perhatian bahawa dalam React sebenar, panggilan komponen fungsi ditangguhkan ke peringkat pembinaan gentian. Walau bagaimanapun, kami berbuat demikian untuk kemudahan, dan ia tidak menjejaskan tujuan siri ini.

Serpihan

Namun, ia masih tidak mencukupi. Sebelum ini, kami hanya menganggap serpihan sebagai div, yang tidak betul. Tetapi jika anda hanya menggantikannya dengan serpihan dokumen, ia tidak akan berfungsi. Sebab untuk ini adalah kerana serpihan adalah bekas sekali sahaja- yang membawa kepada kelakuan aneh- seperti anda tidak boleh mengambil perkara sebenar daripadanya, dan anda tidak boleh menyarangnya, dan banyak perkara pelik (benar-benar, mengapa ia hanya menang ' t berfungsi lebih mudah...). Jadi, sial, kita perlu gali perkara ini.

Jadi penyelesaiannya ialah, kami tidak mencipta DOM untuk serpihan- kami mencari induk yang betul untuk menambah DOM.

Kami perlukan,

div
├── p
└── div
    ├── h1
    └── h2
Salin selepas log masuk
Salin selepas log masuk

Dan tukar pemaparan,

export function render(vDom: VDomNode, parent: HTMLElement) {
    nextUnitOfWork = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: parent
    }
}
Salin selepas log masuk
Salin selepas log masuk

Kini, serpihan itu dikendalikan dengan betul.

Atas ialah kandungan terperinci Bina vDOM Tiny React Chendering. 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!