Let's build a virtual DOM library!
Have you ever wondered how the virtual DOM really works? For me, the best way to learn how something works is to take it apart and rebuild it. If that's how you like to learn, then strap in, because we're about to build a virtual DOM library from scratch.
This guide is intended to be read in order, but the sections are listed below in case you want to skip ahead.
- What is a virtual DOM?
What is a virtual DOM?
You have hopefully already heard of the term virtual DOM, or VDOM. It's a concept where a virtual representation of the Document Object Model (DOM) is built in-memory, and then later synchronised with the "real" DOM. The VDOM concept was pioneered by React, and has since been used as the basis of many other frameworks' rendering systems including Vue.
The original goal of the virtual DOM concept was to allow web developers to render updates to a web page quickly, without making expensive or unnecessary DOM manipulations. This reasoning does not necessarily hold up today, since web browsers have become very efficient at performing DOM manipulations. Regardless, the virtual DOM is still a popular and well-understood rendering mechanism.
All virtual DOM implementations work slightly differently, but the basic concept is the same: plain JavaScript objects are used to represent a tree of virtual DOM nodes. These virtual nodes, or vnodes, are relatively simple:
const vnode = {type: "h1",props: {className: "page-title"},children: [],}
These are the three basic attributes of a vnode:
- type: Sometimes called tag, this is the type of DOM node that will be rendered - often an HTML element name, although as we will find out later, this can be a custom component too.
- props: These are properties that can be set on the DOM node. These props are often named after properties in the JavaScript DOM API, which are sometimes different to the corresponding HTML attribute name. The most common example of this is the
className
prop which represents theclass
attribute in HTML. - children: A list of more vnodes. A value of
null
is also acceptable to represent a vnode with no child nodes.
With this simple object structure, a virtual DOM renderer can traverse a VDOM tree and create a real DOM from it. This process is usually called mounting. To avoid unnecessary changes to the real DOM, most VDOM renderers can compare a VDOM tree against an already-mounted DOM and update just the parts that have changed. React calls this process reconciliation, but other libraries call it diffing or patching.
The lifecycle of a virtual DOM library is therefore quite simple: a web application is made up of components that render a VDOM tree. The VDOM library mounts or patches that tree into the real DOM. The user interacts with the real DOM, triggering events that change the state of the web application's components. The virtual DOM library re-renders the components, and the whole cycle starts over.
React to DOM events┌────────────────────────────────────────────────────────────────────────────┐│ │▼ │┌────────────────────┐ ┌────────────────────┐ ┌──────────┴─────────┐│ │ Render │ │ Mount / Patch │ ││ Component render ├───────────────►│ Virtual DOM ├────────────────►│ Real DOM ││ function │ │ Tree │ │ ││ │ │ │ │ │└────────────────────┘ └────────────────────┘ └────────────────────┘
Before we get our hands dirty and write some code, I need to set your expectations correctly:
- We are going to build a virtual DOM library that can render a relatively complex web application.
- It will not handle any edge cases.
- It will not be fast or efficient.
- You should definitely not use it to build a real web application.
- You will finish this guide with a better understanding of how virtual DOM-based libraries like React and Vue work.
If that sounds reasonable, then let's get started!