How does Virtual DOM work? (Build your own)

aidenybai

Aiden Bai

Posted on June 17, 2021

How does Virtual DOM work? (Build your own)

Plug: I help develop million: <1kb virtual DOM - it's fast!

Introduction

The virtual DOM is a tree of virtual nodes that represents what the DOM looks like. virtual nodes are light, stateless, and are JavaScript objects that only contain necessary fields. virtual nodes can be assembled into trees, and "diffed" to make pinpoint changes to the DOM.

The reasoning behind this is because modification and access of DOM nodes is computationally expensive. A diff between virtual nodes, accessing the DOM only for modification, is the premise of virtual DOM. It avoids the DOM as much as possible, favoring plain JavaScript objects instead, making reading and writing much cheaper.

How does it work?

The Million virtual DOM contains three main functions: m, createElement, patch. To completely understand how virtual DOM works, let's try and create our own rudimentary virtual DOM based off of these functions (~7 minutes read time).

Before we start, we need to define what a virtual node is. A virtual node can either be a JavaScript object (virtual element) or a string (text).


The m function is a helper function that creates virtual elements. A virtual element contains three properties:

  • tag: which stores the tag name of the element as a string.
  • props: which stores the properties/attributes of the element as an object.
  • children: which stores virtual node children of the element as an array.

An example implementation of the m helper function is below:

const m = (tag, props, children) => ({
  tag,
  props,
  children,
});
Enter fullscreen mode Exit fullscreen mode

This way, we can construct virtual nodes easily:

m('div', { id: 'app' }, ['Hello World']);
// Is the same as:
{
  tag: 'div',
  props: { id: 'app' },
  children: ['Hello World']
}
Enter fullscreen mode Exit fullscreen mode

The createElement function turns a virtual node into a real DOM element. This is important because we'll be using this in our patch function and the user may also use it to initialize their application.

We'll need to programmatically create a new detached DOM element, then iterate over the virtual element props while adding them to the DOM element, and finally iterating over the children, initialling them as well. An example implementation of the createElement helper function is below:

const createElement = vnode => {
  if (typeof vnode === 'string') {
    return document.createTextNode(vnode); // Catch if vnode is just text
  }
  const el = document.createElement(vnode.tag);
  if (vnode.props) {
    Object.entries(vnode.props).forEach(([name, value]) => {
      el[name] = value;
    });
  }
  if (vnode.children) {
    vnode.children.forEach(child => {
      el.appendChild(createElement(child));
    });
  }
  return el;
};
Enter fullscreen mode Exit fullscreen mode

This way, we can convert virtual nodes to DOM elements easily:

createElement(m('div', { id: 'app' }, ['Hello World']));
// Is the same as: <div id="app">Hello World</div>
Enter fullscreen mode Exit fullscreen mode

The patch function takes an existing DOM element, old virtual node, and new virtual node. This won't necessarily be the most performant implementation, but this is just for demonstration purposes.

We'll need to diff the two virtual nodes, then replace out the element when needed. We do this by first determining whether one of the virtual nodes is a text, or a string, and replacing it if the old and new virtual nodes do not equate each other. Otherwise, we can safely assume both are virtual elements. After that, we diff the tag and props, and replace the element if the tag has changed. We then iterate over the children and recursively patch if a child is a virtual element. An example implementation of the patch helper function is below:

const patch = (el, oldVNode, newVNode) => {
  const replace = () => el.replaceWith(createElement(newVNode));
  if (!newVNode) return el.remove();
  if (!oldVNode) return el.appendChild(createElement(newVNode));
  // Handle text case
  if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
    if (oldVNode !== newVNode) return replace();
  } else {
    // Diff tag
    if (oldVNode.tag !== newVNode.tag) return replace();
    // Diff props
    if (!oldVNode.props?.some((prop) => oldVNode.props?[prop] === newVNode.props?[prop])) return replace();
    // Diff children
    [...el.childNodes].forEach((child, i) => {
      patch(child, oldVNode.children?[i], newVNode.children?[i]);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

This way, we can patch DOM elements based on virtual nodes easily:

const oldVNode = m('div', { id: 'app' }, ['Hello World']);
const newVNode = m('div', { id: 'app' }, ['Goodbye World']);
const el = createElement(oldVNode);

patch(el, oldVNode, newVNode);
// el will become: <div id="app">Goodbye World</div>
Enter fullscreen mode Exit fullscreen mode

Notes:

  • The old virtual node must always model the DOM element until after it is patched.
  • Generally speaking, applications aren't directly written with these methods, rather they should be abstracted out into components and JSX for simplicity.
  • This is not the same as Million's implementation, rather it is a demonstration to better allow you to understand how the virtual DOM works.

So... What's unique about Million then?

Million provides five major improvements: granular patching, fewer iterative passes, fast text interpolation, keyed virtual nodes, compiler flags.

  • Granular patching: Instead of just replacing the entire element when there is a difference in props or children, only the necessary props are changed.
  • Fewer iterative passes: Million attempts to reduce the amount of passes during diffing, allowing for better time and space complexity.
  • Fast text interpolation: Instead of replacing text nodes with DOM methods, Million uses compiler flags to set the textContent of elements to boost performance.
  • Keyed virtual elements: This allows for the patching algorithm to skip nodes if the new virtual element key is the same as the old one, minimizing the amount of unnecessary work.
  • Compiler Flags: This allows for the patching algorithm to skip condition branches, meaning less work is done.
💖 💪 🙅 🚩
aidenybai
Aiden Bai

Posted on June 17, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Micro Frontends with Vite and Bit
javascript Micro Frontends with Vite and Bit

March 13, 2024

#2 React Query: Infinite Scroll
javascript #2 React Query: Infinite Scroll

January 28, 2024

Power of Javascript Array Methods 🔍🛠️
javascript Power of Javascript Array Methods 🔍🛠️

September 29, 2023

Better API Calls 🎣
javascript Better API Calls 🎣

September 24, 2023

Mastering JavaScript Date and Time 📆
javascript Mastering JavaScript Date and Time 📆

September 22, 2023