Please check out my previous post which explains what the VDOM is if you don't know what the VDOM is.
Alright, now that we know what the VDOM is, let's build it!
So, we need to be able to do 4 things:
- Create a Virtual Node (I'll just call it vnode).
- Mount (load) a VDOM
- Unmount a VDOM
- Patch (compare two vnodes and figure out the difference and then mount)
Alright, let's go!
Creating a vnode
This is basically just a utility function
function createVNode(tag, props = {}, children = []) {
return {tag, props, children}
}
In Vue (and many other places) this function is called
h
, short forhyperscript
. But I choose to give it a better name
Mounting a vnode
By mounting, I mean appending the vnode to any container like #app
or any other place it should be mounted.
This function will recursively walk through all of the nodes' children and mount them to the respective containers.
Note that all the following code is to be placed in the mount function.
function mount(vnode, container) { ... }
1. Create a DOM Element
const element = (vnode.element = document.createElement(vnode.tag))
You may be thinking what vnode.element
is. It's just a small property which is set internally so we can know which element is the parent of the vnode
2. Set all attributes from the props object
We can just loop over them
Object.entries(vnode.props || {}).forEach([key, value] => {
element.setAttribute(key, value)
})
3. Mount the children.
There are two cases to handle:
- The
children
is just text - The
children
are an array of vnodes
if (typeof vnode.children === 'string') {
element.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, element) // Recursively mount the children
})
}
4. Finally, we have to append the stuff to the DOM
container.appendChild(element)
Here is the final result
function mount(vnode, container) {
const element = (vnode.element = document.createElement(vnode.tag))
Object.entries(vnode.props || {}).forEach([key, value] => {
element.setAttribute(key, value)
})
if (typeof vnode.children === 'string') {
element.textContent = vnode.children
} else {
vnode.children.forEach(child => {
mount(child, element) // Recursively mount the children
})
}
container.appendChild(element)
}
Unmounting a vnode
Unmounting is as simple as removing an element from the DOM:
function unmount(vnode) {
vnode.element.parentNode.removeChild(vnode.element)
}
Patching a vnode
This is (only comparatively) the most complex function we have to write. It basically find out the difference between two vnodes and only patch the changes.
This time, I will put the explanation in the code comments, so be sure to read through it!
Let's go!
function patch(VNode1, VNode2) {
// Assign the parent DOM element
const element = (VNode2.element = VNode1.element);
// Now we have to check the difference between the two vnodes
// If the nodes are of different tags, assume that the whole content has changed.
if (VNode1.tag !== VNode2.tag) {
// Just unmount the old node and mount the new node
mount(VNode2, element.parentNode)
unmount(Vnode1)
} else {
// Nodes have same tags
// So we have two checks remaining
// - Props
// - Children
// I am not going to check the props for now because it would just lengthen the post and miss the point. I might write a third article which contains the full implementation
// Checking the children
// If the new node has a string for children
if (typeof VNode2.children == "string") {
// If the two children are **strictly** different
if (VNode2.children !== VNode1.children) {
element.textContent = VNode2.children;
}
} else {
// If the new node has an array of children
// - The length of children is the same
// - The old node has more children than the new one
// - The new node has more children than the old one
// Find out the lengths
const children1 = VNode1.children;
const children2 = VNode2.children;
const commonLen = Math.min(children1.length, children2.length)
// Recursively call patch for all the common children
for (let i = 0; i < commonLen; i++) {
patch(children1[i], children2[i])
}
// If the new node has fewer children
if (children1.length > children2.length) {
children1.slice(children2.length).forEach(child => {
unmount(child)
})
}
// If the new node has more children
if (children2.length > children1.length) {
children2.slice(children1.length).forEach(child => {
mount(child, element)
})
}
}
}
}
And that's it! This is a really basic version of a vdom implementation just so you can grasp the concept. There's a few more stuff left to do, including checking the props and some more speed improvements.
For now, let's render a VDOM!
Let's go back to out really old generateList
example. For our vdom implementation, we could do something like this:
function generateList(list) {
let children = list.map(child => createVNode("li", null, child));
return createVNode("ul", { class: 'fruits-ul' }, children)
}
mount(generateList(["apple", "banana", "orange"]), document.querySelector("#app")
Here is a demo: