In this article I want to recap my experience creating own “virtual DOM”. Sounds too ambitiously? Probably, but it is not that complicated as you might think. As the title states, it will make sense when you create your own, rather than reading thousands of articles about it.
Originally, I was inspired by the talk of stefan judis at Web Rebels 2018, so feel free to take a look at it here.
This concept of Virtual DOM got popular back in 2013 when React was released. Because of this concept ReactJS is one of super fast libraries for building UI. I’ll try to explain it in few sentences, then we will get to writing own one.
Virtual DOM is the representation of DOM as an object. When changes to state of application are made, new Virtual DOM is compared(applying diffing algorithms) with DOM and only changes are reflected, not causing full re-rendering of DOM.
Ok, here is a plan how we will create our virtual DOM.
- Create hyperscript function to render DOM — it is kinda JSX
- Create simple app with hyperscript
- Make our App dynamic and learn to render virtual DOM
- Implement diffing algorithm to see the power of virtual DOM
HyperScript implementation
If you ever worked with ReactJS you probably know what is JSX. It can be another topic for discussion, but shortly it transforms “HTML-like” syntax into JavaScript function calls, in React it is transferred as React.createElement
. So, in this step our aim is to recreate this awesome function.
Generally, it is the building block that creates virtual DOM. However, in this step I want to emphasize how we can build simple DOM with it and in further steps we will develop it to make virtual DOM.
The inputs for this function are: type of node, properties(a.k.a. attributes), children. So here is a simple implementation of this function:
export default function hyperscript(nodeName, attrs, ...children) { const $el = document.createElement(nodeName); for (let key in attrs) { $el.setAttribute(key, attrs[key]); } children.forEach(child => { if (typeof child === 'string') { $el.appendChild(document.createTextNode(child)); } else { $el.appendChild(child); } }); return $el; }
js
Firstly, It simply creates DOM element with nodeName. Secondly, sets its attributes, and last step to append child nodes with the check for text nodes.
Note: actually, we could go further and render children recursively, but I left this for later for simplicity reason.
Here is how it can be used(from now on we will use h
instead of hyperscript
):
const App = () => { return h( 'h1', null, 'Hello, vDOM!' ); }
js
Creating application with Hyperscript
Okay, we now can create simple application with the help of Hyperscript. Let’s create a bit more complex application than it was in previous step. Here is our newer App function.
const App = (props) => { const { list } = props; return h( 'div', { class: 'app' }, h('h1', null, 'Simple vDOM'), h( 'ul', null, ...list.map(item => h( 'li', null, item)) ) ); };
js
When the App is executed it creates a div, with two children: one rendering H1 heading, and the second one unordered list. Note that we pass props to our function and render props.list into unordered list. Let’s add some more rendering magic:
let currentApp; const render = (state) => { const newApp = App(state); currentApp ? document.body.replaceChild(newApp, currentApp) : document.body.appendChild(newApp); currentApp = newApp; } const state = { list: [ '🕺', '💃', '😀', '🙋♀️', '💼', '🕶', '👏', '🤳', '🕵️♀️', '👩🔧', ] }; render(state);
js
Generally, we just want to render the output of App function(that is valid DOM) into the body of document giving the state that contains emojis list.
It wasn’t that hard. Is it? Let’s add some dynamic content, and add random emoji every 1 second this way we can see how our app renders.
setInterval(() => { state.list = [ ...state.list, getRandomItemFromArray(state.list), ]; render(state); }, 1000);
js
Implement vDOM rendering
Okay, now we have dynamic app done with hyperscript let’s move on to actual virtual DOM and its implementation. First of all we need to change our hyperscript function. Now it should not create real DOM, but instead it rather should create virtual DOM. So, given nodeName, attributes and children we just a create an object with corresponding keys. Thanks to ES6 we can do this in one line:
export default function hyperscript(nodeName, attributes, ...children) { return { nameName, attributes, chilren }; }
js
We have a virtual DOM and if we execute the App function with same emojis list we get something like this(logged in console):
Pretty similar to DOM. Now let’s create a function that renders virtual DOM into real DOM. As you might have guessed it should take virtual Node as a parameter. Here it is:
export const renderNode = vnode => { let el; const { nodeName, attributes, children } = vnode; if(vnode.split) return document.createTextNode(children); el = document.createElement(nodeName); for (let key in attributes) { el.setAttribute(key, attributes[key]); } (children || []).forEach(child => el.appendChild(renderNode(child))); return el; };
js
Let me explain what it does step by step:
- Using destructuring we retrieve nodeName, attributes and children of virtual Node
- If vnode is text(we can check it by vnode.split) then we return text Node
- Otherwise we create an element with nodeName and set its attributes from attributes object
- Do the same thing for children if any
Now, remember our render function that rendered our App? We just need to change a little bit to make it work:
const newApp = renderNode(App(state));
js
This article became a bit longer than I thought, so I decided to break into two parts. Here is a second part.
So, let’s recap this. We created a hyperscript — virtual DOM factory, renderNode — that turns virtual DOM into DOM element and a function component App that creates whole page. The result is now the same as we did it before without virtual DOM, but now we have more control. In the next article we will explore what makes React(and virtual DOM) so fast.
You can look up all steps in my GitHub repository. You can find these steps in branches.
In the next article we will implement simple diffing algorithm, that will make our app faster and you’ll be able to see it action.