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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.