「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > 小さな React Chendering vDOM を構築する

小さな React Chendering vDOM を構築する

2024 年 11 月 8 日に公開
ブラウズ:428

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] に連絡して削除してください。
最新のチュートリアル もっと>
  • Java でファイルまたはフォルダーのサイズを効率的に計算するにはどうすればよいですか?
    Java でファイルまたはフォルダーのサイズを効率的に計算するにはどうすればよいですか?
    Java でのファイルまたはフォルダーのサイズの取得ファイルまたはフォルダーのサイズの取得は、ファイルを操作する場合の一般的なタスクです。ジャワでは。これを効果的に行う方法は次のとおりです。ファイル サイズの取得ファイルのサイズを取得するには、java.io で length() メソッドを使用でき...
    プログラミング 2024 年 11 月 8 日に公開
  • 変数 その04
    変数 その04
    মনে করুন আপনি চা খাবেন। না, চা না। কফিই খান। প্রোগ্রামার হচ্ছেন কফি তো খেতেন পারেন। কফিকে প্রোগ্রামারদের সঙ্গি বললে ভুল হবে না । যাই হোক। এখন কফি তৈর...
    プログラミング 2024 年 11 月 8 日に公開
  • React を使い始めたときに知っておきたかったこと
    React を使い始めたときに知っておきたかったこと
    3 年間の React 開発から得た教訓 初めて React に飛び込んだとき、パンドラの箱を開けたような気分でした。学ぶことがたくさんあり、途中でたくさんの「なるほど!」に出会いました。瞬間。ここでは、React を始めるときに知っておきたかった 10 のことを紹介します。これは、React を始...
    プログラミング 2024 年 11 月 8 日に公開
  • Golang でのタイピング速度テスト CLI アプリケーションの作成
    Golang でのタイピング速度テスト CLI アプリケーションの作成
    そのタイトルについては、長く一生懸命考えなければなりませんでした?...話は終わったので、すごいコードを書いてみましょう :) ポンプブレーキ?悲鳴を上げる.... 今日構築しようとしているものについて少し紹介しましょう。タイトルがわかりにくいと思うので、ここでは golang で入力速度を計算する...
    プログラミング 2024 年 11 月 8 日に公開
  • ブートストラップ モーダルが機能しないのはなぜですか? ($(...).modal は関数ではありません)
    ブートストラップ モーダルが機能しないのはなぜですか? ($(...).modal は関数ではありません)
    TypeError: $(...).modal は Bootstrap Modal の関数ではありません次の操作をしようとしているときにこのエラーが発生しましたBootstrap モーダルを HTML に動的に挿入し、jQuery を使用してトリガーします。問題を詳しく調べてみましょう:このエラーは...
    プログラミング 2024 年 11 月 8 日に公開
  • PHP で再帰的な匿名関数を作成するには?
    PHP で再帰的な匿名関数を作成するには?
    再帰的な匿名 PHP 関数の作成PHP で再帰的な匿名関数を作成すると有利な場合があります。以下のコードは、関数を参照として渡してこれを実現する方法を示しています。$factorial = function( $n ) use ( &$factorial ) { if( $n...
    プログラミング 2024 年 11 月 8 日に公開
  • ダブルクリックの表示/非表示ボタンが 2 回目の呼び出しでのみ機能するのはなぜですか?
    ダブルクリックの表示/非表示ボタンが 2 回目の呼び出しでのみ機能するのはなぜですか?
    初回使用時にボタンの表示/非表示をダブルクリックするのはなぜですか?Web ページでは、ボタンは表示または非表示を目的としています。要素ですが、最初の呼び出し時にダブルクリックする必要があります。調べたところ、ボタンのコードは次のとおりであることがわかりました。function showhideme...
    プログラミング 2024 年 11 月 8 日に公開
  • グリッド レイアウト: 初心者のための究極のガイド
    グリッド レイアウト: 初心者のための究極のガイド
    CSS の冒険へようこそ!今日は、Web デザインの武器の中で最も強力なツールの 1 つである CSS グリッド レイアウトについて詳しく説明します。これは、レイアウト技術のスイス アーミー ナイフと考えてください。多用途かつ正確で、Web ページを美しく整理された傑作に変えることができます。グリッ...
    プログラミング 2024 年 11 月 8 日に公開
  • Python 辞書の理解: 完全な概要
    Python 辞書の理解: 完全な概要
    Python 辞書は、Python プログラミングで最も多用途で広く使用されているデータ構造の 1 つです。これらは、開発者がキーと値のペアでデータを保存できるようにする組み込みのデータ型であり、さまざまなアプリケーションで非常に役立ちます。この記事では、辞書とは何か、その使用方法を検討し、その機能...
    プログラミング 2024 年 11 月 8 日に公開
  • H1 タグの最後の単語の色を変更するにはどうすればよいですか?
    H1 タグの最後の単語の色を変更するにはどうすればよいですか?
    H1 の最後の単語の色を変更する解決策Web 開発の分野では、CSS を使用して要素をスタイル設定することは基本的な実践です。ただし、H1 タグ内の最後の単語の色を変更する場合、ネイティブ CSS では不十分です。ただし、従来の CSS の領域を超えた解決策があるため、心配する必要はありません。この...
    プログラミング 2024 年 11 月 8 日に公開
  • Angular Signals と RxJS の新機能
    Angular Signals と RxJS の新機能
    1) Signals vs RxJS: Angular 16 で Signal 変数を最初から作成し、Observable と比較します。 サイドバー メニューとトップ メニューがある例では、ボタンを押すたびに、トップ メニューがサイドバー メニューを折りたたむように指示します。 RxJS の場合:...
    プログラミング 2024 年 11 月 8 日に公開
  • malloc() と free() の実装 — 古いメモリが最初に再利用されます
    malloc() と free() の実装 — 古いメモリが最初に再利用されます
    malloc() と free() の実装に関するこのシリーズの前の post では、新しいブロックを解放することでメモリ ブロックを再利用し、ヒープを削減する方法を示しました。ただし、現在の関数には微妙な問題があります。新しいブロックの再利用が優先されるため、時間の経過とともにメモリ消費量が増加す...
    プログラミング 2024 年 11 月 8 日に公開
  • Java におけるカプセル化と抽象化: 究極のガイド
    Java におけるカプセル化と抽象化: 究極のガイド
    Java またはオブジェクト指向プログラミング (OOP) 言語を学習する場合、カプセル化と抽象化という 2 つの重要な概念が際立ちます。これらの概念は、コードの再利用性、セキュリティ、保守性を促進する OOP の重要な柱です。これらは一緒に使用されることが多いですが、異なる目的を果たします。 この...
    プログラミング 2024 年 11 月 8 日に公開
  • ZustandのソースコードのcreateWithEqualityFnImplについて説明しました。
    ZustandのソースコードのcreateWithEqualityFnImplについて説明しました。
    この記事では、理解を深めるために値の一部をログに記録することで、createWithEqualityFnImpl がどのように実装されるかを分析します。 上の画像からわかるように、createWithEqualityFn は関数 createWithEqualityFnImpl を呼び出します。この...
    プログラミング 2024 年 11 月 8 日に公開
  • CSV を保存するときに Pandas がインデックス列を追加しないようにするにはどうすればよいですか?
    CSV を保存するときに Pandas がインデックス列を追加しないようにするにはどうすればよいですか?
    Pandas を使用して保存された CSV のインデックス列を回避するPandas を使用して変更を加えた後に CSV ファイルを保存するとき、デフォルトの動作では、インデックス列。これを回避するには、to_csv() メソッドを使用するときにインデックス パラメーターを False に設定します。...
    プログラミング 2024 年 11 月 8 日に公開

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3