」工欲善其事,必先利其器。「—孔子《論語.錄靈公》
首頁 > 程式設計 > 建造一個小型 React Chendering vDOM

建造一個小型 React Chendering vDOM

發佈於2024-11-08
瀏覽:694

Build a Tiny React Chendering vDOM

This tutorial is based on this tutorial, but with JSX, typescript and an easier approach to implement. You can checkout the notes and code on my GitHub repo.

This part we will render the vDOM to the actual DOM. In addition, we will also introduce fiber tree, which a core structure in React.

Rendering vDOM

Rendering vDOM is, simple- too simple. You need to know the following web native APIs.

  • document.createElement(tagName: string): HTMLElement Creates an actual DOM element.
  • document.createTextNode(text: string): Text Creates a text node.
  • .appendChild(child: Node): void Appends a child node to the parent node. A method on HTMLElement
  • .removeChild(child: Node): void Removes a child node from the parent node. A method on HTMLElement
  • .replaceChild(newChild: Node, oldChild: Node): void Replaces a child node with a new child node. A method on HTMLElement
  • .replaceWith(...nodes: Node[]): void Replaces a node with new nodes. A method on Node
  • .remove(): void Removes a node from the document. A method on Node
  • .insertBefore(newChild: Node, refChild: Node): void Inserts a new child node before a reference child node. A method on HTMLElement
  • .setAttribute(name: string, value: string): void Sets an attribute on an element. A method on HTMLElement.
  • .removeAttribute(name: string): void Removes an attribute from an element. A method on HTMLElement.
  • .addEventListener(type: string, listener: Function): void Adds an event listener to an element. A method on HTMLElement.
  • .removeEventListener(type: string, listener: Function): void Removes an event listener from an element. A method on HTMLElement.
  • .dispatchEvent(event: Event): void Dispatches an event on an element. A method on HTMLElement.

Woa, a bit too much, right? But all you need to do is mirroring the creation of vDOM to the actual DOM. Here is a simple example.

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)
        }
    }
}

We registered properties starting with on as event listeners, this is a common practice in React. Also, we ignored the key property, which is used for reconciliation, not for rendering.

Okay, so rendering done and this chapter ends...? No.

Idle Time Rendering

In real react, the rendering process is a bit more complicated. To be more specific, it will use requestIdleCallback, to make more urgent tasks to be done first, lowering its own priority.

Please note that requestIdleCallback is not supported on Safari, on both MacOS and iOS (Apple Engineers, please, why? At least they are working on it, at 2024). If you are on a Mac, use chrome, or replace it with a simple setTimeout. In real react, it uses scheduler to handle this, but the basic idea is the same.

To do so, we need to know the following web native APIs.

  • requestIdleCallback(callback: Function): void Requests a callback to be called when the browser is idle. The callback will be passed an IdleDeadline object. The callback will have a deadline argument, which is an object with the following properties.
    • timeRemaining(): number Returns the time remaining in milliseconds before the browser is no longer idle. So we should finish our work before the time is up.

So we need to split our rendering in chunks, and use requestIdleCallback to handle it. A simple way would be to just render one node at a time. It is easy- but do not be eager to do so- or you'll waste a lot of time, since we also need other work to be done while rendering.

But we can have the following code as a basic framework for what we are going to do.

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() 



If you now fill // TODO with rendering vDOM, and return the next vDOM node to be rendered, you can have a simple idle time rendering. But don't be hasty- we need more work.

Fiber Tree

In the next chapter, we will implement reactivity, and the reconciliation is rather complicated- so we move some content into this part, that is the fiber tree.

Fiber tree is just a special data structure. When react handles changes, it does the following process.

  1. Something, may be a user, or initial rendering, triggers a change.
  2. React creates a new vDOM tree.
  3. React calculate the new fiber tree.
  4. React calculates the difference between the old fiber tree and the new fiber tree.
  5. React applies the difference to the actual DOM.

You can see, fiber tree is essential for React.

The fiber tree, a little bit different from traditional tree, has three types of relations between nodes.

  • child of: A node is a child of another node. Please note that, in fiber tree, every node can have only one child. The traditional tree structure is represented by a child with many siblings.
  • sibling of: A node is a sibling of another node.
  • parent of: A node is a parent of another node. Different from child of, many nodes can share the same parent. You can think parent node in fiber tree as a bad parent, who only cares about the first child, but is still, in fact, parent of many children.

For example, for the following DOM,

We can represent it as a tree.

div
├── p
└── div
    ├── h1
    └── h2

p is a child of the root div, but the secondary div is not a child of the root div, but a sibling of p. h1 and h2 are children of the secondary div.

When it comes to rendering, the order is mainly depth-first, but kind of different- so basically, it follows these rules. For each node, it goes through the following steps.

  1. If this node has a unprocessed child, process the child.
  2. If this node has a sibling, process the sibling. Repeat until all siblings are processed.
  3. Mark this node as processed.
  4. Process its parent.

Now let's implement that. But first, we need to trigger the rendering process. It is simple- just set the nextUnitOfWork to the root of the fiber tree.

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

After triggering the rendering, browser will call performUnitOfWork, this is where we, well, perform the work.

The first is that we need to create actual DOM elements. We can do this by creating a new DOM element, and append it to the parent DOM element.

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')
    }
}
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')
}

This is the first part of the work. Now we need to construct the fiber branching out from the current one.

const fiber = nextUnitOfWork
if (isElement(fiber.vDom)) {
    const elements = fiber.vDom.children ?? []
    let index = 0
    let prevSibling = null

    while (index 



Now we have a fiber tree built for the current node. Now let's follow our rules to process the fiber tree.

if (fiber.child) {
    return fiber.child
}
let nextFiber: Fiber | null = fiber
while (nextFiber) {
    if (nextFiber.sibling) {
        return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
}
return null

Now we can render the vDOM, here it is. Please note that typescript is being stupid here since it can not tell the type of our virtual DOM, we need an ugly bypass here.

import { render } from "./runtime";
import { createElement, fragment, VDomNode } from "./v-dom";

function App() {
    return 
        

a

b

> } const app = document.getElementById('app') const vDom: VDomNode = App() as unknown as VDomNode render(vDom, app!)

Now your vDOM is rendered to the actual DOM. Congratulations! You have done a great job. But we are not done yet.

Cumulative Commit

There will be a problem with the current implementation- if we have too many nodes that slows the whole process down, the user will see how the rendering is done. Of course, it won't leak commercial secrets or something, but it is not a good experience. We'd rather hide the dom creation behind the curtain, the submit it all at once.

The solution is simple- instead of directly committing to the document, we create an element without adding it to the document, and when we are done, we add it to the document. This is called cumulative commit.

let wip: Fiber | null = null
let wipParent: HTMLElement | null = null

export function render(vDom: VDomNode, parent: HTMLElement) {
    wip = {
        parent: null,
        sibling: null,
        child: null,
        vDom: vDom,
        dom: null,
    }
    wipParent = parent
    nextUnitOfWork = wip
}

Now, we remove the appendChild from performUnitOfWork, that is, the following part,

if(nextUnitOfWork.parent && nextUnitOfWork.parent.dom) {
    nextUnitOfWork.parent.dom.appendChild(nextUnitOfWork.dom)
}

Now if we finish all the work, we have all the fiber correctly built up with their DOM, but they are not added to the document. When such event dispatches, we call a commit function, which will add the DOM to the document.

function commit() {

}

function workLoop(deadline: IdleDeadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    if(!nextUnitOfWork && wip) {
        commit()
    }
    shouldYield = deadline.timeRemaining() 



Now, the commit function is simple- just add all the children DOM recursively to the wip, then commit wip to the DOM.

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent.dom.appendChild(fiber.dom)
        }
        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip = null
}

You can test this out by adding a timeout to commitChildren function. previously, the rendering was done step by step, but now it is done all at once.

Nested Components

You may try nested functions- like the following,

import { render } from "./runtime";
import { createElement, fragment, VDomNode } from "./v-dom";

function App() {
    return 
        

a

b

c

> } function Wrapper() { return

a

b

} const app = document.getElementById('app') const vDom: VDomNode = Wrapper() as unknown as VDomNode render(vDom, app!)

But it won't work, since when parsing the JSX, tag is just the label name. Sure, for native elements, it is just a string, but for components, it is a function. So in the process of converting JSX to vDOM, we need to check if the tag is a function, and if so, call it.

export function createElement(tag: string | Function, props: VDomAttributes, ...children: VDomNode[]): VDomElement {
    if (tag instanceof Function) {
        return tag(props, children)
    }
    return {
        kind: tag === '' ? 'fragment' : 'element',
        tag,
        children,
        props: props ?? {},
        key: props?.key ?? undefined
    }
}

Now, props and children are required for each component. In real React, they added extra field to check- you can imagine, just by replacing functions with classes, so you have extra fields- then you provide new function to create objects, a typical factory pattern- but we take a lazy we here.

import { render } from "./runtime";
import { createElement, fragment, VDomAttributes, VDomNode } from "./v-dom";

type FuncComponent = (props: VDomAttributes, children: VDomNode[]) => JSX.Element

const Wrapper: FuncComponent = (_: VDomAttributes, __: VDomNode[]) => {
    return 

aa

>
} const app = document.getElementById('app') const vDom: VDomNode = Wrapper({}, []) as unknown as VDomNode console.log(vDom) render(vDom, app!)

Please note that in the real React, the function component call is delayed to the fiber building stage. Nonetheless, we did so for convenience, and it doesn't really harm the purpose of this series.

Fragment

However, it's still not enough. Previously, we just treated fragment as div, which is not correct. But if you just replace that with a document fragment, it won't work. The reason for this is because fragments is a one-time container- which leads to a strange behaviour- like you cannot take real things out of it, and you can not nest them, and many strange things (really, why it just won't work simpler...). So, fuck, we need to dig this shit up.

So the solution is that, we do not create DOM for fragment- we find the correct parent to add the DOM.

We need,

export function isFragment(value: VDomNode): value is VDomElement {
    return isElement(value) && value.kind === 'fragment'
}

And change the rendering,

function commit() {
    function commitChildren(fiber: Fiber | null) {
        if(!fiber) {
            return
        }
        if(fiber.dom && fiber.parent?.dom) {
            fiber.parent?.dom?.appendChild(fiber.dom)
        }

        if(fiber.dom && fiber.parent && isFragment(fiber.parent.vDom)) {
            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!)
        }

        commitChildren(fiber.child)
        commitChildren(fiber.sibling)
    }
    commitChildren(wip)
    wipParent?.appendChild(wip!.dom!)
    wip = null
}

Now, the fragment is correctly handled.

版本聲明 本文轉載於:https://dev.to/fingerbone/build-a-tiny-react-ch2-rendering-vdom-f7f?1如有侵犯,請聯繫[email protected]刪除
最新教學 更多>
  • 為什麼我在Silverlight Linq查詢中獲得“無法找到查詢模式的實現”錯誤?
    為什麼我在Silverlight Linq查詢中獲得“無法找到查詢模式的實現”錯誤?
    查詢模式實現缺失:解決“無法找到”錯誤在Silverlight應用程序中,嘗試使用LINQ建立LINQ連接以錯誤而實現的數據庫”,無法找到查詢模式的實現。”當省略LINQ名稱空間或查詢類型缺少IEnumerable 實現時,通常會發生此錯誤。 解決問題來驗證該類型的質量是至關重要的。在此特定實例...
    程式設計 發佈於2025-05-09
  • 如何從PHP中的Unicode字符串中有效地產生對URL友好的sl。
    如何從PHP中的Unicode字符串中有效地產生對URL友好的sl。
    為有效的slug生成首先,該函數用指定的分隔符替換所有非字母或數字字符。此步驟可確保slug遵守URL慣例。隨後,它採用ICONV函數將文本簡化為us-ascii兼容格式,從而允許更廣泛的字符集合兼容性。 接下來,該函數使用正則表達式刪除了不需要的字符,例如特殊字符和空格。此步驟可確保slug僅包...
    程式設計 發佈於2025-05-09
  • 您可以使用CSS在Chrome和Firefox中染色控制台輸出嗎?
    您可以使用CSS在Chrome和Firefox中染色控制台輸出嗎?
    在javascript console 中顯示顏色是可以使用chrome的控制台顯示彩色文本,例如紅色的redors,for for for for錯誤消息? 回答是的,可以使用CSS將顏色添加到Chrome和Firefox中的控制台顯示的消息(版本31或更高版本)中。要實現這一目標,請使用以下...
    程式設計 發佈於2025-05-09
  • 如何使用Regex在PHP中有效地提取括號內的文本
    如何使用Regex在PHP中有效地提取括號內的文本
    php:在括號內提取文本在處理括號內的文本時,找到最有效的解決方案是必不可少的。一種方法是利用PHP的字符串操作函數,如下所示: 作為替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式來搜索特...
    程式設計 發佈於2025-05-09
  • 如何使用Java.net.urlConnection和Multipart/form-data編碼使用其他參數上傳文件?
    如何使用Java.net.urlConnection和Multipart/form-data編碼使用其他參數上傳文件?
    使用http request 上傳文件上傳到http server,同時也提交其他參數,java.net.net.urlconnection and Multipart/form-data Encoding是普遍的。 Here's a breakdown of the process:Mu...
    程式設計 發佈於2025-05-09
  • JavaScript計算兩個日期之間天數的方法
    JavaScript計算兩個日期之間天數的方法
    How to Calculate the Difference Between Dates in JavascriptAs you attempt to determine the difference between two dates in Javascript, consider this s...
    程式設計 發佈於2025-05-09
  • PHP陣列鍵值異常:了解07和08的好奇情況
    PHP陣列鍵值異常:了解07和08的好奇情況
    PHP數組鍵值問題,使用07&08 在給定數月的數組中,鍵值07和08呈現令人困惑的行為時,就會出現一個不尋常的問題。運行print_r($月)返回意外結果:鍵“ 07”丟失,而鍵“ 08”分配給了9月的值。 此問題源於PHP對領先零的解釋。當一個數字帶有0(例如07或08)的前綴時,PHP將...
    程式設計 發佈於2025-05-09
  • CSS可以根據任何屬性值來定位HTML元素嗎?
    CSS可以根據任何屬性值來定位HTML元素嗎?
    靶向html元素,在CSS 中使用任何屬性值,在CSS中,可以基於特定屬性(如下所示)基於特定屬性的基於特定屬性的emants目標元素: 字體家庭:康斯拉斯(Consolas); } 但是,出現一個常見的問題:元素可以根據任何屬性值而定位嗎?本文探討了此主題。 的目標元素有任何任何屬性值,...
    程式設計 發佈於2025-05-09
  • 如何使用PHP將斑點(圖像)正確插入MySQL?
    如何使用PHP將斑點(圖像)正確插入MySQL?
    essue VALUES('$this->image_id','file_get_contents($tmp_image)')";This code builds a string in PHP, but the function call fil...
    程式設計 發佈於2025-05-09
  • 如何將來自三個MySQL表的數據組合到新表中?
    如何將來自三個MySQL表的數據組合到新表中?
    mysql:從三個表和列的新表創建新表 答案:為了實現這一目標,您可以利用一個3-way Join。 選擇p。 *,d.content作為年齡 來自人為p的人 加入d.person_id = p.id上的d的詳細信息 加入T.Id = d.detail_id的分類法 其中t.taxonomy ...
    程式設計 發佈於2025-05-09
  • 在細胞編輯後,如何維護自定義的JTable細胞渲染?
    在細胞編輯後,如何維護自定義的JTable細胞渲染?
    在JTable中維護jtable單元格渲染後,在JTable中,在JTable中實現自定義單元格渲染和編輯功能可以增強用戶體驗。但是,至關重要的是要確保即使在編輯操作後也保留所需的格式。 在設置用於格式化“價格”列的“價格”列,用戶遇到的數字格式丟失的“價格”列的“價格”之後,問題在設置自定義單元...
    程式設計 發佈於2025-05-09
  • 反射動態實現Go接口用於RPC方法探索
    反射動態實現Go接口用於RPC方法探索
    在GO 使用反射來實現定義RPC式方法的界面。例如,考慮一個接口,例如:鍵入myService接口{ 登錄(用戶名,密碼字符串)(sessionId int,錯誤錯誤) helloworld(sessionid int)(hi String,錯誤錯誤) } 替代方案而不是依靠反射...
    程式設計 發佈於2025-05-09
  • HTML格式標籤
    HTML格式標籤
    HTML 格式化元素 **HTML Formatting is a process of formatting text for better look and feel. HTML provides us ability to format text without us...
    程式設計 發佈於2025-05-09
  • C++成員函數指針正確傳遞方法
    C++成員函數指針正確傳遞方法
    如何將成員函數置於c [&& && && && && && && && && && &&&&&&&&&&&&&&&&&&&&&&&華儀的函數時,在接受成員函數指針的函數時,要在函數上既要提供指針又可以提供指針和指針到函數的函數。需要具有一定簽名的功能指針。要通過成員函數,您需要同時提供對象指針(此...
    程式設計 發佈於2025-05-09
  • 如何為PostgreSQL中的每個唯一標識符有效地檢索最後一行?
    如何為PostgreSQL中的每個唯一標識符有效地檢索最後一行?
    postgresql:為每個唯一標識符在postgresql中提取最後一行,您可能需要遇到與數據集合中每個不同標識的信息相關的信息。考慮以下數據:[ 1 2014-02-01 kjkj 在數據集中的每個唯一ID中檢索最後一行的信息,您可以在操作員上使用Postgres的有效效率: id dat...
    程式設計 發佈於2025-05-09

免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。

Copyright© 2022 湘ICP备2022001581号-3