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

构建一个小型 React Chendering vDOM

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

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]删除
最新教程 更多>
  • 如何解决使用 JavaScript 更改 iframe src 的问题
    如何解决使用 JavaScript 更改 iframe src 的问题
    使用 JavaScript 更改 iframe src:疑难解答单击单选按钮时更改 iframe 的 src 属性时遇到问题。要纠正此问题,必须检查代码以确定确切的原因。一个可能的问题是括号的错误使用。在您的代码中,该行:document.getElementById['calendar'].src...
    编程 发布于2024-11-08
  • 为什么 `window.onscroll` 在 iPhone/iPad 上不起作用?
    为什么 `window.onscroll` 在 iPhone/iPad 上不起作用?
    在 iPhone/iPad 上使用滚动事件捕获事件尝试在 iPad 上捕获滚动事件时,故障排除工作揭示了常见的方法例如 window.onscroll 和 document.onscroll 无法触发所需的响应。理解 iOS 上的事件处理设备iPhoneOS 事件处理机制与传统桌面浏览器不同。在连续...
    编程 发布于2024-11-08
  • 从开发人员到审阅者:初级开发人员审阅数据库查询的清单
    从开发人员到审阅者:初级开发人员审阅数据库查询的清单
    作为开发人员,提供高质量的代码至关重要,这些代码不仅具有功能性,而且还针对性能进行了优化。在开发人员领域的三年里,我从一名实践开发人员转变为审阅者角色。我在审核过程中关注的关键领域之一是数据库查询优化。 为什么关注数据库查询? 数据库查询可以显着影响应用程序的性能。编写得好的查询可以有效地获取数据,...
    编程 发布于2024-11-08
  • Mockito 是最好的 Java 模拟框架吗?  对其优缺点的综合评价。
    Mockito 是最好的 Java 模拟框架吗? 对其优缺点的综合评价。
    最佳 Java 模拟框架:Mockito在 Java 中,制作模拟对象对于有效的单元测试至关重要。鉴于选择过多,为此目的确定最佳框架可能会令人畏惧。本文评估了最突出的选择之一 Mockito,重点介绍了它的优点和缺点。Mockito 因其用户友好的语法而脱颖而出,使其易于开发人员使用。其简化方法针对...
    编程 发布于2024-11-08
  • 如何可靠地获取当前运行的Python文件的路径?
    如何可靠地获取当前运行的Python文件的路径?
    如何获取当前执行的Python文件的路径问题:确定当前运行的Python文件的路径可能很麻烦,特别是当遇到在特定场景下证明不可靠的方法时。其中包括从另一个脚本或在 IDLE 或 Mac OS X v10.6 等特定环境中启动执行的实例。解决方案:通用获取当前执行的 Python 的文件路径文件,采用...
    编程 发布于2024-11-08
  • Stack Overflow 如何创建这些信息丰富的弹出消息?
    Stack Overflow 如何创建这些信息丰富的弹出消息?
    复制 Stack Overflow 的弹出消息功能您可能已经注意到 Stack Overflow 上出现的时尚且内容丰富的弹出消息。这些消息为用户提供了有价值的通知和指导,您可能想知道如何在自己的网站上实现类似的功能。Stack Overflow 利用 HTML、CSS 和 JavaScript 的...
    编程 发布于2024-11-08
  • 为什么 Python 中没有元组理解?
    为什么 Python 中没有元组理解?
    理解 Python 中元组推导式的缺失在 Python 编程语言中,列表推导式和字典推导式提供了生成结构化数据的有效方法。然而,缺乏元组理解是一个异常现象。本文深入探讨了这一遗漏背后的原因。元组不变性是原因的假设并不成立。元组确实是不可变的,但这个属性并不妨碍它们在推导式中构建。问题的关键在于 Py...
    编程 发布于2024-11-08
  • 如何使用 VLC 模块在 Python 中播放 MP3 歌曲?
    如何使用 VLC 模块在 Python 中播放 MP3 歌曲?
    使用 Python 播放 MP3 歌曲使用正确的工具,在 Python 中播放 MP3 歌曲可以非常简单。错误的做法:尝试使用wave模块打开MP3文件,如下图所示不推荐:import wave w = wave.open("e:/LOCAL/Betrayer/Metalik Klinik...
    编程 发布于2024-11-08
  • 如何为Apache PHP应用程序配置环境变量?
    如何为Apache PHP应用程序配置环境变量?
    Apache PHP 应用程序的环境变量配置开发依赖环境变量的 PHP 应用程序时,必须清楚地了解如何配置环境变量使用 Apache 时设置这些变量。本文旨在提供有关配置可在 PHP 中访问的环境变量的指导,确保 Web 应用程序的正确运行。具体来说,为同一服务器中的各个域配置单独的环境变量是一种常...
    编程 发布于2024-11-08
  • 如何从 Activity 访问 ViewPager 片段方法?
    如何从 Activity 访问 ViewPager 片段方法?
    从 Activity 访问 ViewPager Fragment 方法许多移动应用程序使用片段,即代表模块化屏幕部分的独立组件。使用视图分页器管理多个片段可实现流畅的导航和页面动画。有时,开发人员需要在片段中执行特定操作以响应外部事件,例如用户在视图寻呼机上滑动。然而,实现此功能可能会遇到某些挑战。...
    编程 发布于2024-11-08
  • 如何在 Python 中按列值对散点图着色?
    如何在 Python 中按列值对散点图着色?
    按列值对散点图着色在 Python 中,Matplotlib 库提供了多种自定义散点图美观的方法。一项常见任务是根据特定列中的值分配颜色。Seaborn 集成一种解决方案是利用基于 Matplotlib 构建的 Seaborn 库。 Seaborn 提供 sns.relplot 和 sns.Face...
    编程 发布于2024-11-08
  • 为什么 fmt.Printf 显示负整数的二进制表示与 Go 中预期的不同?
    为什么 fmt.Printf 显示负整数的二进制表示与 Go 中预期的不同?
    二进制补码和 fmt.Printf:解开二进制表示之谜处理有符号整数时,计算机使用二进制补码来表示负值。这与典型的二进制表示不同,其中符号由单独的位指示。例如,在二进制补码中,整数 -5 表示为 1111 1011。但是,使用 fmt.Printf 打印二进制表示形式可能会产生意外结果。例如,以下代...
    编程 发布于2024-11-08
  • 读取控制台输入
    读取控制台输入
    InputStream读取方法: read():允许您直接从流中读取字节。 read()的三个版本: int read():读取单个字节并在流末尾返回-1。 int read(byte data[]):读取字节,直到数据数组填满、到达流末尾或发生错误。返回读取的字节数,如果到达流末尾则返回 -1。 ...
    编程 发布于2024-11-08
  • PHP 构造函数属性推广初学者指南
    PHP 构造函数属性推广初学者指南
    PHP 8 引入了一个名为 构造函数属性提升 的奇妙功能。如果您是 PHP 或一般编程新手,这可能听起来有点复杂。但别担心!本博客将通过大量编码示例向您介绍它是什么、为什么有用以及如何使用它。开始吧! 什么是建筑商财产促销? 在 PHP 8 之前,创建具有属性的类并在构造函数中初始化...
    编程 发布于2024-11-08
  • 如何使用 CNTLM 访问工作场所代理后面的 pip?
    如何使用 CNTLM 访问工作场所代理后面的 pip?
    与 CNTLM 的 PIP 代理连接要使用 CNTLM 访问工作场所代理后面的 pip,用户可能会遇到 --proxy 选项的问题。然而,利用环境变量提供了可靠的解决方案。CNTLM 配置验证可以通过运行“cntlm.exe -c cntlm.ini -I -M http://google.com”...
    编程 发布于2024-11-08

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

Copyright© 2022 湘ICP备2022001581号-3