Anatomy of a Virtual DOM
Why we represent the DOM structure in JavaScript first, and how an array-based Virtual DOM solves the visual coding problem.
In the previous post, we looked at how one-way data binding gives us a predictable way to update the user interface. We passed all our logic through a single dataToView function. If data changed, the view updated automatically.
But there was a lingering problem. The code we wrote to create these elements was incredibly imperative. It was a long list of line-by-line instructions.
Code that creates visual elements should ideally look visual itself. When we use HTML, we are using a beautifully visual, declarative language. The tags map directly to what we see on the screen. JavaScript, on the other hand, gives us nothing but getter and setter properties. But what if we could build a visual representation of our user interface right inside our JavaScript?
The problem with imperative DOM code
Let's say we want to show a simple greeting. In vanilla JavaScript, building this is a manual, step-by-step process.
let name = "Durgesh";
let jsDiv = document.createElement("div");
jsDiv.textContent = `Hello, ${name}!`;
document.body.appendChild(jsDiv);This works, but it takes three steps to do something that HTML does in one. As we build more features, we end up stacking hundreds of these createElement and appendChild lines. The structure of our application becomes completely hidden inside blocks of logic.
We want our code to reflect the shape of the interface. We want a visual map.
Building a visual map in JavaScript
What if we could represent an HTML element in JavaScript using a simple data structure? An array, for example. The first item in the array could be the element type, and the second item could be the content.
let name = "Durgesh";
let divInfo = ["div", `Hello, ${name}!`];This is starting to look a lot better. It is short, and it mimics the structure of an element. But divInfo is just an array. The browser has no idea what to do with it. We need a way to translate our visual map into real C++ DOM elements.
We can write a generic convert function to do exactly that.
function convert(node) {
// node[0] is the element type, e.g. "div"
let elem = document.createElement(node[0]);
// node[1] is the text content
elem.textContent = node[1];
return elem;
}
let myNewDiv = convert(["div", "Hello, Durgesh!"]);Our convert function takes our visual array and runs the tedious, imperative DOM commands for us. We get to write declarative, visual arrays. The computer does the heavy lifting.
Assembling the Virtual DOM
We can now describe our entire user interface as a collection of these arrays. This collection is our Virtual DOM representing the whole structure of the page entirely in JavaScript memory.
Let's build a createVDOM function. It will look at our current data and return an array of element arrays.
let name = "";
function createVDOM() {
return [
["input", name, handleInput],
["div", `Hello, ${name}!`]
];
}Now, every time the user types a new letter, we just need to run createVDOM again. It will return a fresh visual map representing the new state of the world. By having this intermediary step, we separate the "what it should look like" from the "how we actually update the browser".
Closing the loop
Of course, the user still needs to see the result. We need a function that takes our Virtual DOM map and actually paints it onto the real DOM.
function updateDOM() {
// Generate the new visual map based on current data
let vDOM = createVDOM();
// Convert our arrays into real DOM objects
let jsInput = convert(vDOM[0]);
let jsDiv = convert(vDOM[1]);
// Paint them to the screen
document.body.replaceChildren(jsInput, jsDiv);
}If we run updateDOM inside an interval every 15 milliseconds, we have successfully created an end-to-end component.
- The user types something. Our data updates.
createVDOMsketches a new blueprint.convertbuilds the raw materials.replaceChildrenrepaints the house.
This gives us profound control. Our code is visual, predictable, and fully determined by our data.
But there is a massive catch. Using replaceChildren every 15 milliseconds means we are throwing away perfectly good DOM elements and building brand new ones from scratch over and over again. If our application has a thousand elements, the browser will severely struggle.
In the next post, we will explore how to solve this performance problem by getting smart about what we choose to repaint.
Further Reading and Watching
- Video: What is the Virtual DOM? -- Academind
- Article / Docs: Virtual DOM and Internals (React Docs)
Practice what you just read.
Keep reading