Frontend Development 18 min read

Implementing a Simple Virtual DOM Library with Patch and Diff Algorithms

This article explains how to build a lightweight virtual DOM library in JavaScript, covering the creation of VNode objects, the patch and diff algorithms, handling of attributes, classes, styles, and events, and demonstrates a step‑by‑step implementation using the Snabbdom approach.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Implementing a Simple Virtual DOM Library with Patch and Diff Algorithms

Creating Virtual DOM Objects

Virtual DOM (VNode) is a plain JavaScript object that describes a real DOM node's tag, children, text, and a reference to the actual element. The h function builds such objects, handling string/number children as text and normalizing non‑array children.

{
    tag: '',
    children: [],
    text: '',
    el: null
}
export const h = (tag, children) => {
    let text = ''
    let el
    // children is a text node
    if (typeof children === 'string' || typeof children === 'number') {
        text = children
        children = undefined
    } else if (!Array.isArray(children)) {
        children = undefined
    }
    return {
        tag,
        children,
        text,
        el
    }
}

Examples:

h('div', 'I am text')
h('div', [h('span')])

Patch Process

The patch function compares an old VNode with a new VNode and updates the real DOM with the minimal changes. The first call may receive a real DOM element as the old VNode, which is converted to a VNode before proceeding.

export const patch = (oldVNode, newVNode) => {
    // dom element
    if (!oldVNode.tag) {
        let el = oldVNode
        el.innerHTML = ''
        oldVNode = h(oldVNode.tagName.toLowerCase())
        oldVNode.el = el
    }
    // further processing...
}

When the tags are identical, patchVNode handles children, text, and attribute updates; otherwise the old node is replaced.

const patchVNode = (oldNode, newNode) => {
    if (oldVNode === newVNode) {
        return
    }
    if (oldVNode.tag === newVNode.tag) {
        // same tag – patch
    } else {
        // different tag – replace
        let newEl = createEl(newVNode)
        let parent = oldVNode.el.parentNode
        parent.insertBefore(newEl, oldVNode.el)
        parent.removeChild(oldVNode.el)
    }
}

Creating Real DOM Elements

const createEl = (vnode) => {
    let el = document.createElement(vnode.tag)
    vnode.el = el
    // create children
    if (vnode.children && vnode.children.length > 0) {
        vnode.children.forEach(item => {
            el.appendChild(createEl(item))
        })
    }
    // create text node
    if (vnode.text) {
        el.appendChild(document.createTextNode(vnode.text))
    }
    return el
}

Diff Algorithm (Double‑Ended)

The double‑ended diff compares nodes from both ends of the old and new child lists, using four pointers (oldStartIdx, oldEndIdx, newStartIdx, newEndIdx). It first tries to match nodes at the same positions, then cross‑matches (start‑end, end‑start). If a match is found, patchVNode is called and pointers are moved.

const diff = (el, oldChildren, newChildren) => {
    let oldStartIdx = 0
    let oldEndIdx = oldChildren.length - 1
    let newStartIdx = 0
    let newEndIdx = newChildren.length - 1
    let oldStartVNode = oldChildren[oldStartIdx]
    let oldEndVNode = oldChildren[oldEndIdx]
    let newStartVNode = newChildren[newStartIdx]
    let newEndVNode = newChildren[newEndIdx]

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isSameNode(oldStartVNode, newStartVNode)) {
            // patch and move pointers
        } else if (isSameNode(oldStartVNode, newEndVNode)) {
            // patch, move oldStartVNode after oldEndVNode
        } else if (isSameNode(oldEndVNode, newStartVNode)) {
            // patch, move oldEndVNode before oldStartVNode
        } else if (isSameNode(oldEndVNode, newEndVNode)) {
            // patch, move pointers
        } else {
            // search for a reusable node or create a new one
        }
    }
    // remove remaining old nodes or insert remaining new nodes
}

Attribute Updates

Attributes, classes, styles, and events are passed through the data argument of h . The patch process calls dedicated helpers to update each type.

Class Names

export const h = (tag, data = {}, children) => {
    let key
    if (data && data.key) {
        key = data.key
    }
    return { tag, children, text, el, key, data }
}
const updateClass = (el, newVNode) => {
    el.className = ''
    if (newVNode.data && newVNode.data.class) {
        let className = ''
        Object.keys(newVNode.data.class).forEach(cla => {
            if (newVNode.data.class[cla]) {
                className += cla + ' '
            }
        })
        el.className = className
    }
}

Styles

const updateStyle = (el, oldVNode, newVNode) => {
    let oldStyle = oldVNode.data.style || {}
    let newStyle = newVNode.data.style || {}
    // remove old styles not present in new
    Object.keys(oldStyle).forEach(item => {
        if (newStyle[item] === undefined || newStyle[item] === '') {
            el.style[item] = ''
        }
    })
    // add or update new styles
    Object.keys(newStyle).forEach(item => {
        if (oldStyle[item] !== newStyle[item]) {
            el.style[item] = newStyle[item]
        }
    })
}

Other Attributes

const updateAttr = (el, oldVNode, newVNode) => {
    let oldAttr = oldVNode && oldVNode.data.attr ? oldVNode.data.attr : {}
    let newAttr = newVNode.data.attr || {}
    Object.keys(oldAttr).forEach(item => {
        if (newAttr[item] === undefined || newAttr[item] === '') {
            el.removeAttribute(item)
        }
    })
    Object.keys(newAttr).forEach(item => {
        if (oldAttr[item] !== newAttr[item]) {
            el.setAttribute(item, newAttr[item])
        }
    })
}

Events

const removeEvent = (oldVNode) => {
    if (oldVNode && oldVNode.data && oldVNode.data.event) {
        Object.keys(oldVNode.data.event).forEach(item => {
            oldVNode.el.removeEventListener(item, oldVNode.data.event[item])
        })
    }
}
const updateEvent = (el, oldVNode, newVNode) => {
    let oldEvent = oldVNode && oldVNode.data.event ? oldVNode.data.event : {}
    let newEvent = newVNode.data.event || {}
    // unbind removed or changed events
    Object.keys(oldEvent).forEach(item => {
        if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {
            el.removeEventListener(item, oldEvent[item])
        }
    })
    // bind new or changed events
    Object.keys(newEvent).forEach(item => {
        if (oldEvent[item] !== newEvent[item]) {
            el.addEventListener(item, newEvent[item])
        }
    })
}

Summary

The article walks through a complete, minimal virtual DOM implementation, illustrating how VNodes are created, how the patch and double‑ended diff algorithms efficiently update the real DOM, and how classes, styles, attributes, and events are synchronized. The code can be adapted to non‑browser platforms by abstracting DOM operations.

frontendJavaScriptPatchVirtual DOMDiff AlgorithmSnabbdomVNode
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.