”工欲善其事,必先利其器。“—孔子《论语.录灵公》
首页 > 编程 > 构建一个小型 React Chendering vDOM

构建一个小型 React Chendering vDOM

发布于2024-11-08
浏览:632

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]删除
最新教程 更多>
  • 为什么我的CSS背景图像出现?
    为什么我的CSS背景图像出现?
    故障排除:CSS背景图像未出现 ,您的背景图像尽管遵循教程说明,但您的背景图像仍未加载。图像和样式表位于相同的目录中,但背景仍然是空白的白色帆布。而不是不弃用的,您已经使用了CSS样式: bockent {背景:封闭图像文件名:背景图:url(nickcage.jpg); 如果您的html,css...
    编程 发布于2025-03-11
  • 如何使用Regex在PHP中有效地提取括号内的文本
    如何使用Regex在PHP中有效地提取括号内的文本
    php:在括号内提取文本在处理括号内的文本时,找到最有效的解决方案是必不可少的。一种方法是利用PHP的字符串操作函数,如下所示: 作为替代 $ text ='忽略除此之外的一切(text)'; preg_match('#((。 &&& [Regex使用模式来搜索特...
    编程 发布于2025-03-11
  • 如何使用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-03-11
  • 可以在纯CS中将多个粘性元素彼此堆叠在一起吗?
    可以在纯CS中将多个粘性元素彼此堆叠在一起吗?
    [2这里: https://webthemez.com/demo/sticky-multi-header-scroll/index.html </main> <section> { display:grid; grid-template-...
    编程 发布于2025-03-11
  • 如何使用组在MySQL中旋转数据?
    如何使用组在MySQL中旋转数据?
    在关系数据库中使用mySQL组使用mySQL组进行查询结果,在关系数据库中使用MySQL组,转移数据的数据是指重新排列的行和列的重排以增强数据可视化。在这里,我们面对一个共同的挑战:使用组的组将数据从基于行的基于列的转换为基于列。 Let's consider the following ...
    编程 发布于2025-03-11
  • 为什么我会收到MySQL错误#1089:错误的前缀密钥?
    为什么我会收到MySQL错误#1089:错误的前缀密钥?
    mySQL错误#1089:错误的前缀键错误descript [#1089-不正确的前缀键在尝试在表中创建一个prefix键时会出现。前缀键旨在索引字符串列的特定前缀长度长度,可以更快地搜索这些前缀。了解prefix keys `这将在整个Movie_ID列上创建标准主键。主密钥对于唯一识别...
    编程 发布于2025-03-11
  • 如何使用FormData()处理多个文件上传?
    如何使用FormData()处理多个文件上传?
    )处理多个文件输入时,通常需要处理多个文件上传时,通常是必要的。 The fd.append("fileToUpload[]", files[x]); method can be used for this purpose, allowing you to send multi...
    编程 发布于2025-03-11
  • 如何克服PHP的功能重新定义限制?
    如何克服PHP的功能重新定义限制?
    克服PHP的函数重新定义限制在PHP中,多次定义一个相同名称的函数是一个no-no。尝试这样做,如提供的代码段所示,将导致可怕的“不能重新列出”错误。 但是,PHP工具腰带中有一个隐藏的宝石:runkit扩展。它使您能够灵活地重新定义函数。 runkit_function_renction_re...
    编程 发布于2025-03-11
  • 大批
    大批
    [2 数组是对象,因此它们在JS中也具有方法。 切片(开始):在新数组中提取部分数组,而无需突变原始数组。 令ARR = ['a','b','c','d','e']; // USECASE:提取直到索引作...
    编程 发布于2025-03-11
  • 如何在Java字符串中有效替换多个子字符串?
    如何在Java字符串中有效替换多个子字符串?
    在java 中有效地替换多个substring,需要在需要替换一个字符串中的多个substring的情况下,很容易求助于重复应用字符串的刺激力量。 However, this can be inefficient for large strings or when working with nu...
    编程 发布于2025-03-11
  • 为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    为什么Microsoft Visual C ++无法正确实现两台模板的实例?
    The Mystery of "Broken" Two-Phase Template Instantiation in Microsoft Visual C Problem Statement:Users commonly express concerns that Micro...
    编程 发布于2025-03-11
  • 如何检查对象是否具有Python中的特定属性?
    如何检查对象是否具有Python中的特定属性?
    方法来确定对象属性存在寻求一种方法来验证对象中特定属性的存在。考虑以下示例,其中尝试访问不确定属性会引起错误: >>> a = someClass() >>> A.property Trackback(最近的最新电话): 文件“ ”,第1行, attributeError:SomeClass实...
    编程 发布于2025-03-11
  • 如何干净地删除匿名JavaScript事件处理程序?
    如何干净地删除匿名JavaScript事件处理程序?
    删除匿名事件侦听器将匿名事件侦听器添加到元素中会提供灵活性和简单性,但是当要删除它们时,可以构成挑战,而无需替换元素本身就可以替换一个问题。 element? element.addeventlistener(event,function(){/在这里工作/},false); 要解决此问题,请考虑...
    编程 发布于2025-03-11
  • 如何使用不同数量列的联合数据库表?
    如何使用不同数量列的联合数据库表?
    合并列数不同的表 当尝试合并列数不同的数据库表时,可能会遇到挑战。一种直接的方法是在列数较少的表中,为缺失的列追加空值。 例如,考虑两个表,表 A 和表 B,其中表 A 的列数多于表 B。为了合并这些表,同时处理表 B 中缺失的列,请按照以下步骤操作: 确定表 B 中缺失的列,并将它们添加到表的末...
    编程 发布于2025-03-11
  • Java是否允许多种返回类型:仔细研究通用方法?
    Java是否允许多种返回类型:仔细研究通用方法?
    在Java中的多个返回类型:一种误解类型:在Java编程中揭示,在Java编程中,Peculiar方法签名可能会出现,可能会出现,使开发人员陷入困境,使开发人员陷入困境。 getResult(string s); ,其中foo是自定义类。该方法声明似乎拥有两种返回类型:列表和E。但这确实是如此吗...
    编程 发布于2025-03-11

免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。

Copyright© 2022 湘ICP备2022001581号-3