How to Build a Simple Virtual DOM in 300–400 Lines of Code
This article explains how to implement a basic Virtual DOM algorithm in 300–400 lines of JavaScript, covering state‑management motivations, tree representation, diffing, and patching, with step‑by‑step code examples and visual illustrations to help developers understand and build their own lightweight Virtual DOM library.
Preface
This article shares a complete implementation of a basic Virtual DOM algorithm using roughly 300–400 lines of code and explains the underlying concepts so readers can deepen their understanding of Virtual DOM and apply it to modern front‑end development.
Thoughts on Front‑End Application State Management
Consider a sortable table where users can click column headers to change the sort order. Storing the current sort key, sort direction, and table data directly in JavaScript quickly leads to many state variables and frequent DOM updates, which become hard to maintain as the application grows.
Instead of manually updating the DOM after each state change, a binding mechanism can automatically synchronize the view with the state. This is the idea behind MVVM and Virtual DOM, which adds extra steps to avoid rebuilding the entire DOM tree.
Virtual DOM Algorithm
Represent a DOM tree as plain JavaScript objects, then render it into real DOM nodes.
var sortKey = "new"; // sort field
var sortType = 1; // 1 for asc, -1 for desc
var data = [{...}, {...}, {...}]; // table dataDefine an Element constructor to model a node:
function Element(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
module.exports = function(tagName, props, children) {
return new Element(tagName, props, children);
};Example of building a virtual ul list:
var el = require('./element');
var ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
]);Render the virtual node to a real DOM element:
Element.prototype.render = function() {
var el = document.createElement(this.tagName);
var props = this.props;
for (var propName in props) {
el.setAttribute(propName, props[propName]);
}
var children = this.children || [];
children.forEach(function(child) {
var childEl = (child instanceof Element)
? child.render()
: document.createTextNode(child);
el.appendChild(childEl);
});
return el;
};Now ulRoot = ul.render(); can be appended to document.body.
Step (2): Diff Two Virtual DOM Trees
The core of Virtual DOM is the diff algorithm, which compares two trees and records differences. A depth‑first traversal assigns a unique index to each node, and only nodes at the same depth are compared, reducing complexity to O(n).
function diff(oldTree, newTree) {
var index = 0;
var patches = {};
dfsWalk(oldTree, newTree, index, patches);
return patches;
}
function dfsWalk(oldNode, newNode, index, patches) {
patches[index] = [];
diffChildren(oldNode.children, newNode.children, index, patches);
}
function diffChildren(oldChildren, newChildren, index, patches) {
var leftNode = null;
var currentNodeIndex = index;
oldChildren.forEach(function(child, i) {
var newChild = newChildren[i];
currentNodeIndex = leftNode && leftNode.count ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1;
dfsWalk(child, newChild, currentNodeIndex, patches);
leftNode = child;
});
}Patch types are defined as constants:
var REPLACE = 0;
var REORDER = 1;
var PROPS = 2;
var TEXT = 3;Examples of recorded patches:
// replace a node
patches[0] = [{ type: REPLACE, node: newNode }];
// change props
patches[0] = [{ type: PROPS, props: { id: "container" } }];
// update text
patches[2] = [{ type: TEXT, content: "Virtual DOM2" }];
// reorder children
patches[0] = [{ type: REORDER, moves: [{...}, {...}] }];Step (3): Apply Patches to the Real DOM
function patch(node, patches) {
var walker = { index: 0 };
dfsWalk(node, walker, patches);
}
function dfsWalk(node, walker, patches) {
var currentPatches = patches[walker.index];
var len = node.childNodes ? node.childNodes.length : 0;
for (var i = 0; i < len; i++) {
var child = node.childNodes[i];
walker.index++;
dfsWalk(child, walker, patches);
}
if (currentPatches) {
applyPatches(node, currentPatches);
}
}
function applyPatches(node, currentPatches) {
currentPatches.forEach(function(currentPatch) {
switch (currentPatch.type) {
case REPLACE:
node.parentNode.replaceChild(currentPatch.node.render(), node);
break;
case REORDER:
reorderChildren(node, currentPatch.moves);
break;
case PROPS:
setProps(node, currentPatch.props);
break;
case TEXT:
node.textContent = currentPatch.content;
break;
default:
throw new Error('Unknown patch type ' + currentPatch.type);
}
});
}Conclusion
The three essential functions— element, diff, and patch —constitute a minimal Virtual DOM implementation. A complete example builds a virtual tree, renders it, creates a new tree, diffs the two, and patches the real DOM accordingly.
// 1. Build virtual DOM
var tree = el('div', {id: 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtual dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
]);
// 2. Render to real DOM
var root = tree.render();
document.body.appendChild(root);
// 3. Create a new virtual DOM
var newTree = el('div', {id: 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtual dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
]);
// 4. Diff the trees
var patches = diff(tree, newTree);
// 5. Apply patches to the real DOM
patch(root, patches);In practice, additional concerns such as event handling and JSX syntax can be integrated to create a more complete React‑like library.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
