Understanding Virtual DOM: A Simple Implementation Guide
>> Table of Contents
In modern web development, efficient rendering is crucial for creating responsive applications. One of the most popular approaches to improving performance is the Virtual DOM. In this blog post, we’ll explore what the Virtual DOM is, why it’s beneficial, and how to implement a basic version in JavaScript.
What is Virtual DOM?
The Virtual DOM is an abstraction of the actual DOM (Document Object Model) that allows web applications to update the UI efficiently. Instead of directly manipulating the real DOM, which can be slow and resource-intensive, we can manipulate a lightweight copy—the Virtual DOM. This approach enables quick calculations and minimal updates to the real DOM, resulting in better performance.
Why Use Virtual DOM?
- Performance: Updating the real DOM is costly. By using the Virtual DOM, we minimize direct manipulations, leading to faster rendering.
- Efficiency: The Virtual DOM uses a diffing algorithm to determine what has changed, allowing us to update only the parts of the DOM that need to change.
- Declarative Syntax: Using a Virtual DOM makes it easier to describe the UI state. You define what the UI should look like at any point in time, and the Virtual DOM takes care of the updates.
Building a Simple Virtual DOM
Let’s dive into implementing a simple Virtual DOM from scratch. We’ll cover the following steps:
- Creating the Virtual DOM representation.
- Rendering the Virtual DOM to the Real DOM.
- Implementing a diffing algorithm.
- Patching the Real DOM with the computed differences.
Step 1: Define a Virtual DOM Structure
We start by creating a function to represent Virtual DOM nodes:
function h(type, props = {}, ...children) {
return { type, props, children };
}
type
: The type of the element (e.g.,div
,span
).props
: An object representing attributes (e.g.,id
,className
).children
: The child elements of the node.
Step 2: Rendering the Virtual DOM
Next, we create a function to render a Virtual DOM node to a real DOM node:
function render(vNode) {
if (typeof vNode === "string") {
return document.createTextNode(vNode);
}
const element = document.createElement(vNode.type);
for (const [key, value] of Object.entries(vNode.props)) {
element.setAttribute(key, value);
}
vNode.children.map(render).forEach((child) => element.appendChild(child));
return element;
}
Step 3: Implementing the Diffing Algorithm
Now, we need to compare the old Virtual DOM with the new one. This is done through a diffing function:
function diff(oldVNode, newVNode) {
if (!newVNode) {
return { type: "REMOVE" };
}
if (
typeof oldVNode !== typeof newVNode ||
(typeof oldVNode === "string" && oldVNode !== newVNode) ||
oldVNode.type !== newVNode.type
) {
return { type: "REPLACE", newVNode };
}
if (oldVNode.type) {
const propChanges = [];
const allProps = { ...oldVNode.props, ...newVNode.props };
for (const key in allProps) {
if (oldVNode.props[key] !== newVNode.props[key]) {
propChanges.push({ key, value: newVNode.props[key] });
}
}
const childrenChanges = [];
const maxLen = Math.max(oldVNode.children.length, newVNode.children.length);
for (let i = 0; i < maxLen; i++) {
childrenChanges.push(diff(oldVNode.children[i], newVNode.children[i]));
}
return { type: "UPDATE", propChanges, childrenChanges };
}
}
Step 4: Patching the Real DOM
Finally, we apply the changes to the real DOM using the patch
function:
function patch(parent, patches, index = 0) {
if (!patches) return;
const element = parent.childNodes[index];
switch (patches.type) {
case "REMOVE":
parent.removeChild(element);
break;
case "REPLACE":
parent.replaceChild(render(patches.newVNode), element);
break;
case "UPDATE":
for (const { key, value } of patches.propChanges) {
if (value == null) {
element.removeAttribute(key);
} else {
element.setAttribute(key, value);
}
}
patches.childrenChanges.forEach((childPatch, i) => {
patch(element, childPatch, i);
});
break;
}
}
Bringing It All Together
To see the Virtual DOM in action, we can create an initial Virtual DOM, render it, and then update it:
Create Initial Virtual DOM:
const vDom1 = h( "div", { id: "container" }, h("h1", {}, "Hello World"), h("p", {}, "This is a virtual DOM example.") );
Render It:
const root = document.getElementById("app"); const realDom = render(vDom1); root.appendChild(realDom);
Update the Virtual DOM and Patch:
const vDom2 = h( "div", { id: "container" }, h("h1", {}, "Hello Virtual DOM!"), h("p", {}, "This example demonstrates updating the DOM efficiently.") ); const patches = diff(vDom1, vDom2); patch(root, patches);
Conclusion
By implementing a simple Virtual DOM, we gain a better understanding of how libraries like React optimize UI updates. The Virtual DOM helps manage changes in a performant manner, allowing developers to build rich and interactive applications without sacrificing speed.
Feel free to modify and experiment with this implementation to explore the power of the Virtual DOM!