Explained and created a simple virtual DOM from scratch
Erick Sosa Garcia
Posted on April 14, 2020
When I first heard about the virtual DOM, I was wondering how it worked and how to create my own virtual DOM. after doing a little research and practice I will show the virtual dom that I created.
What is the dom?
Document Object Model (DOM) is a way to represent the webpage in the structured hierarchical way so that it will become easier for programmers and users to glide through the document. With DOM, we can easily access and manipulate tags, IDs, classes, Attributes or Elements using commands or methods provided by Document object.
Why called as Object Model ?
Documents are modeled using objects, and the model includes not only the structure of a document but also the behavior of a document and the objects of which it is composed of like tag elements with attributes in HTML.
Structure of DOM:
DOM can be thought of as Tree or Forest(more than one tree). The term structure model is sometimes used to describe the tree-like representation of a document. One important property of DOM structure models is structural isomorphism: if any two DOM implementations are used to create a representation of the same document, they will create the same structure model, with precisely the same objects and relationships.
What is Virtual DOM?
the virtual DOM is an in-memory representation of the real DOM elements in a object. Example:
const myButton = {
tagName: 'button',
attrs: {
id: 'btn',
class: 'save-btn'
},
children: ['save']
};
html equivalent
<button id="btn" class="save-btn">save</button>
Understanding all this let's start 😊
we need a function to create an object that represents the elements and return this object
// createElement.js
function createElement(tagName, { attrs = {}, children = [] } = {}){
return {
tagName,
attrs,
children
}
}
export default createElement;
now we need create a function to render the element
// render.js
function render({ tagName, attrs = {}, children = [] }){
let element = document.createElement(tagName);
// insert all children elements
children.forEach( child => {
if (typeof child === 'string'){
// if the children is a kind of string create a text Node object
element.appendChild(document.createTextNode(child));
}
else {
// repeat the process with the children elements
element.appendChild(render(child));
}
});
// if it has attributes it adds them to the element
if (Object.keys(attrs).length){
for (const [key, value] of Object.entries(attrs)) {
element.setAttribute(key, value);
}
}
return element;
};
export default render;
then create a function to insert the element into the DOM
// insert.js
function insertElement(element, domElement){
domElement.replaceWith(element);
return element;
}
export default insertElement;
Now that we have the tools, let's try them out!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>my vDOM</title>
</head>
<body>
<div id="root">
</div>
<script src="./main.js" type="module"></script>
</body>
</html>
// main.js
import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';
let myVirtualElement = createElement("div", {
attrs: { id: "container" },
children: [
createElement("p", {
attrs: { id: "text" },
children: ["hello world"],
}),
]
});
let element = render(myVirtualElement);
let rootElemet = insertElement(element, document.querySelector('#root'));
run this on any web server, i run it with live server in vscode
We got it! 🥳
Now we can make it more interesting, taking the algorithm to make differences between virtual elements created by Jason Yu in this Post.
// diff.js
import render from './render.js';
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.max(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttrs = (oldAttrs, newAttrs) => {
const patches = [];
// set new attributes
for (const [k, v] of Object.entries(newAttrs)) {
patches.push($node => {
$node.setAttribute(k, v);
return $node;
});
}
// remove old attributes
for (const k in oldAttrs) {
if (!(k in newAttrs)) {
patches.push($node => {
$node.removeAttribute(k);
return $node;
});
}
}
return $node => {
for (const patch of patches) {
patch($node);
}
};
};
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
oldVChildren.forEach((oldVChild, i) => {
childPatches.push(diff(oldVChild, newVChildren[i]));
});
const additionalPatches = [];
for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
additionalPatches.push($node => {
$node.appendChild(render(additionalVChild));
return $node;
});
}
return $parent => {
for (const [patch, child] of zip(childPatches, $parent.childNodes)) {
patch(child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
};
};
const diff = (vOldNode, vNewNode) => {
if (vNewNode === undefined) {
return $node => {
$node.remove();
return undefined;
};
}
if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
if (vOldNode !== vNewNode) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
} else {
return $node => undefined;
}
}
if (vOldNode.tagName !== vNewNode.tagName) {
return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttrs(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
now we change main.js
// main.js
import createElement from './createElement.js';
import render from './render.js';
import insertElement from './insert.js';
import diff from './diff.js';
let myElement = createElement('div', {
attrs: { class: 'container'},
children: [createElement('img', {
attrs: { id: 'img', src: 'https://i.picsum.photos/id/1/200/300.jpg' },
children: []
})]
})
let element = render(myElement);
let rootElemet = insertElement(element, document.querySelector('#root'));
let count = 0;
setInterval(()=> {
count += 1;
let myVirtualElemet = createElement('div', {
attrs: { class: 'img'},
children: [createElement('img', {
attrs: { id: 'img', src: `https://i.picsum.photos/id/${count}/200/300.jpg` },
children: []
})]
})
const patch = diff(myElement, myVirtualElemet);
rootElemet = patch(rootElemet);
myElement = myVirtualElemet;
}, 1000);
run it 🤞
We got it! 🥳
every second we change the src attribute with a new id inside the link so it updates and applies the changes in the DOM.
Sorry for writing so bad i don't speak english
Posted on April 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.