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.
Merender vDOM adalah, mudah- terlalu mudah. Anda perlu mengetahui API asli web berikut.
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) } } }
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.
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.
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) } } }
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.
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.
Anda boleh lihat, pokok gentian adalah penting untuk React.
Pokok gentian, sedikit berbeza daripada pokok tradisional, mempunyai tiga jenis perhubungan antara nod.
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') }
Kita boleh mewakilinya sebagai pokok.
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
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.
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) } } }
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') }
<div> <p></p> <div> <h1></h1> <h2></h2> </div> </div>
Ini adalah bahagian pertama kerja. Sekarang kita perlu membina gentian bercabang daripada yang sekarang.
div ├── p └── div ├── h1 └── h2
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 } }
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') } }
Kini vDOM anda dipaparkan kepada DOM sebenar. tahniah! Anda telah melakukan kerja yang hebat. Tetapi kami belum selesai.
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') }
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++ } }
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
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!)
Anda boleh menguji ini dengan menambahkan tamat masa pada fungsi commitChildren. sebelum ini, pemaparan dilakukan secara berperingkat, tetapi kini ia dilakukan serentak.
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) } } }
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') }
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>
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.
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
Dan tukar pemaparan,
export function render(vDom: VDomNode, parent: HTMLElement) { nextUnitOfWork = { parent: null, sibling: null, child: null, vDom: vDom, dom: parent } }
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!