DEV Community

Building a Simple Virtual DOM from Scratch

YCM Jason on December 05, 2018

I gave a live-coding talk last week at the Manchester Web Meetup #4. I built a virtual DOM from scratch in less than an hour during the talk. I...
Collapse
 
thobyv profile image
Thoby V ijishakin

This is the kind of article that makes me feel so Lucky to be alive at this moment and be part of this code side of the world which is filled with extremely passionate and knowledgeable people who really make efforts to share their knowledge with others.

It's because of engineers like you that it's actually approachable to not only learn to code, but learn to make amazing things.

Forgive me for saying so much, I just happen to really appreciate this, even if I'm not thinking of building a Vdom right now.. Knowing how things work behind the wheel feels very good.

Thank you.

Collapse
 
ycmjason profile image
YCM Jason

It's because of readers like you that make me feel motivated to keep writing! Your comment almost made me teared 😭! Thank you so much!!!! ♥️♥️♥️

Collapse
 
thobyv profile image
Thoby V ijishakin

Thank you! Awesome article again!

Collapse
 
mudgen profile image
Nick Mudge

I love your comment!

Collapse
 
lski profile image
Lee C

Hey, really great write up. I missed the talk at Manchester Web Meetup, but wanted to see this talk, when I first learned React a few years I looked at how the Virtual DOM was put together and did a similar createElement/mount function pair, but I think the real power on the Virtual DOM, especially when used in React is the diffing mechanism. I didnt attempt that at the time, but I think what you have put above is great because I think it helps show why/how frameworks like React update themselves in reaction to changes and you do it with understandable and clean code, awesome :D

Collapse
 
ycmjason profile image
YCM Jason

Thank you so much! Writing clean and elegant code is my passion. I tried to keep things clean so that people can understand it.

Collapse
 
lski profile image
Lee C

I'm the same, I think there is a lot to be gained from clean code, not just for yourself, but for other developers that do (or will) work with it too :) Doing it in example/tutorial code even more so as it improves the value and quality of the article, like you have here :)

Thread Thread
 
ycmjason profile image
YCM Jason

I am so glad I found someone who appreciate this. ❤❤

Collapse
 
mgkh profile image
Kaung Htet

Last whole week i am researching about reactive, and virtual dom to make my hobby javascript library. I am stacking around creating virtual dom and diffing. I saw your post
and it help me alot. I'm really thank you for your explanation and i saw you in video and you're so active in that and full of energy. And again Thank you :)

 
ycmjason profile image
YCM Jason • Edited

Sure. You have stepped into the common trap of thinking recursion recursively. Your brain will stack-overflow first before the computer does haha.

The reason why you are so confused is because you are lacking the "leap of faith" for recursion. You try to figure out what is happening, then you look into the function; it calls itself, and u look into the function again... Then you are lost.

All you need is faith!

The first thing is to define what diff and diffChildren do. I made it very clear for diff.

Imagine we have a function diff (oldVTree, newVTree) which calculate the differences between the two virtual trees; return a patch($tree) function that takes in the real DOM of oldVTree and perform appropriate operations to the real DOM to make the real DOM looks like newVTree.

So the idea is, you know diff will somehow call itself again at some point. And when this happens, all you need is the "leap of faith"; believe that diff will work as you wish! Don't look into diff again to try to figure things out! Just think about what diff should do and return. By our definition, it will return a patch! So just assume the recursive calls to diff will work and read on.

Teaching recursion using this example is a bit hard, have a look at this article which I explained very clearly how you could obtain faith in recursion.

The leap of faith takes some practice to get use to it. If you want some exercise, I am happy to give you some challenge and guide you through them. Feel free to DM me on twitter: @ycmjason

Collapse
 
innocentperos profile image
innocentperos • Edited

thank you very much for such a simplified explanation and elegant code. I have been wanting to understand how vuejs virtual dom works.

Then i recreated your idea i was mind blown by the patch($node) function i was first confuse by how this functions know exactly which element to update on the actual DOM, until i when through the zip function over and over before i understood it.

I have currently implemented it in typescript added reactiveness, event listener.

Once again thank you some much, its individuals like you that make coding magical yet understandable 🤩

Collapse
 
ycmjason profile image
YCM Jason

Thank you for your kind words. ❤️

Collapse
 
ycmjason profile image
YCM Jason

Thank you so much for your kind words! I am very very glad to see you enjoying the post!

I used to think that the virtual DOM was some kind of evil magic I'd never understand, but now I can confidently say that I do.

This is one of the main reasons why I did this topic!! I am glad this presentation achieved its goal!

Please ask the questions! Would be nice if I can clear things further!

Collapse
 
precosmicowl profile image
Kirill Vasiltsov • Edited

Very good job. Thank you for a thorough guide!
Actually, there is a typo in your code within the post.

  const additionalPatches = [];
  for (const additionalVChild of newVChildren.slice(oldVChildren.length)) {
    additionalPatches.push($node => {
      $node.appendChild(render(newVChildren));
      return $node;
    });
  }
Enter fullscreen mode Exit fullscreen mode

Inside the function that we put into additionalPatches we should render not the newVChildren but the additionalVChild.
This code appears in the first example in the explanation of diffChildren function.

Collapse
 
ycmjason profile image
YCM Jason

You are a hero! Thanks for pointing that out! 🎉🎉🎉 I'll change it as soon as possible!

Collapse
 
maiermic profile image
Michael Maier

This typo is still in the article 😉

Collapse
 
to_fl profile image
Tom F.

Great content. I am wondering why you don't use Proxy objects instead of this complex diff function.

Collapse
 
ycmjason profile image
YCM Jason

Because Proxy wasn't ready at the time of writing this article. How do you think Proxy can be used here to improve this?

Collapse
 
to_fl profile image
Tom F.

You can create a proxy object out of a regular object, then you can listen to any change to the object and execute some code against those changes : javascript.info/proxy

Collapse
 
johnqueen99 profile image
johnqueen99

Thank you for the wonderful guide

Collapse
 
victormeneghini profile image
Victor Meneghini

Hey! This article is gold (:

I didn't even notice the time pass while reading! I will follow u to check your contents! Well done my friend :D

Collapse
 
seydel1847 profile image
Mariusz

Is it '0 to oldVChildren.length' or should that be '0 to oldVChildren.length-1'?

Collapse
 
ycmjason profile image
YCM Jason

nice catch! I will update!

Collapse
 
mart_e profile image
Martin

Hi,

Thanks a lot for the article, very nicely explained.
How would you recommend to handle events on the virtual nodes (e.g. add a addEventListener on a node that will influence another node rendering)?

Collapse
 
ycmjason profile image
YCM Jason

since many people asked. i might write an eps 2 that could cover this.

Collapse
 
csmrdb profile image
Casimir de Bruijn • Edited

Please do, if you haven't already. I enjoyed your article btw, pinned on reading list.

Currently, I have the following somewhat working:

render.ts
if (events) {
events.map(([type, event]) => {
$element.addEventListener(type, event)
})
}

index.ts
attrs: {...},
events: [['click',() => {console.log('event handled')}]]

It seems to work and not to propagate upwards. Not sure about dynamically rendering nested elements or w/e. Thoughts?

Collapse
 
estengrove profile image
Steven G.

Couldn’t agree more with this comment. I looked into Reacts vdom a while back to better understand and I just got lost and lost. This post was first time I’d seen someone breakdown concisely and in a way that doesn’t overwhelm the reader with “frivolous” complexities

Well done thanks, Jason!

Collapse
 
nicks101 profile image
Nikki Goel

Amazing. I did go through the whole and implemented it myself also.
Can I ask for an article or video or both on a similar topic - "Create your own bindings using proxies from scratch".
It was originally requested in the comments of the YouTube video.

Collapse
 
ycmjason profile image
YCM Jason

bindings as in reactivity?

Collapse
 
nicks101 profile image
Nikki Goel

Yes. Reactivity in Vue (how DOM updates and render the changes, how changes are detected, etc).
Also how computed properties and watchers work.
I would love to see your take on this.

Collapse
 
maiermic profile image
Michael Maier

The version with

$parent.childNodes.forEach(($child, i) => {
    childPatches[i]($child as HTMLElement)
})
Enter fullscreen mode Exit fullscreen mode

does not work correctly if child nodes are removed, since live NodeList ($parent.childNodes) updates the collection automatically when the DOM changes (e.g. child is removed). Hence, forEach is not called for all (original) elements (child nodes). For example, if there are 3 child nodes and the first patch keeps the child and the second and third patch delete the child, then only the first and second patches are called. The third patch is not called, since the second patch removes the node, which removes it from $parent.childNodes.

It is fixed by the version that uses zip, since it copies the elements of $parent.childNodes into an Array, which is not automatically updated when the DOM changes.

Collapse
 
samuanv profile image
Samuel Andreo

I could attend to the talk and it was perfect. I really appreciate and value the ability to share knowledge on such an interesting and cutting-edge topic as this. Thanks mate ;)

Collapse
 
ycmjason profile image
YCM Jason

❤❤❤ thank you so much!!!!

Collapse
 
ycmjason profile image
YCM Jason

share the post if you liked it! spread the knowledge! :)

 
ycmjason profile image
YCM Jason • Edited

I am sorry that my explanation didn't help. :(

diff has two base cases:

  1. If the new node is undefined
  2. If the new and old nodes are of different types. This could be either of the cases below:
    1. one of the node is a TextNode while the other one is an ElementNode
    2. Both are ElementNode but with different tag.

In fact, all my base cases are defined in a guard clause. This means that all the return statement before the last return can be considered as base case.

Thread Thread
 
ycmjason profile image
YCM Jason

Oops, I just realised there is one more, which is when there is no children in the node. But I didn't explicitly deal with that case as it will be automatically dealt with in the for loop in diffChildren

 
ycmjason profile image
YCM Jason • Edited

did you write this code simply based on the "leap of faith"?

Yes. Totally based on the leap of faith. It always work! It's the very important thing you need when dealing with recursion.

Is this how algorithms like merge sort and quick were written?

Well, merge sort and quick sort if written in a recursive way, can be reasoned about using the "leap of faith" for sure. Whether or not the original Author has the leap of faith there is noway to find out. 😂😂

Is leap of faith good enough for serious professional/interview problems?

Leap of faith will definitely work in professional and interview problems. It's just a mindset you should have when writing recursive solutions, not really a method. Once you do more recursion, you will become confident enough to hold that faith all time.

Collapse
 
maiermic profile image
Michael Maier

The linked CodeSandbox examples don't run. I get

Cannot read property 'replaceWith' of null

It seems that a differnt index.html is used in the sandbox. Hence, the target element #app to mount to is not found

let $rootEl = mount($app, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode
Collapse
 
maiermic profile image
Michael Maier

Thank you very much for the article and video. Great explanation from scratch. I noticed that the GIF animation does not play till the end. Every time count changes, the animation is reset, although the image DOM element is not changed. Does anyone know the reason?

Collapse
 
ant profile image
Ant

really appreciate your sharing, I want to translate this article to Chinese and share to other people in China, I wonder if you agree me to do that, thank you so much。

Collapse
 
ycmjason profile image
YCM Jason

That's a great idea. As long as you credit me, feel free! Please let me proof read before you post it! :)

Collapse
 
kolaveridi profile image
satyajeet kumar jha

Halfway through the article but you are real genius .Great contribution .

Collapse
 
ycmjason profile image
YCM Jason

Thank you!!! Thanks for taking the time to read through this. This is a very very long article I know. Should have made this a series of articles really haha.

Collapse
 
kolaveridi profile image
satyajeet kumar jha

Any way to get in touch with you Jason .Would love to learn things from you .

Thread Thread
 
ycmjason profile image
YCM Jason

Just feel free to DM me on twitter. @ycmjason

Collapse
 
lednhatkhanh profile image
Nhat Khanh

Found this, will spend my weekends to go through this for sure, thank you.

Collapse
 
chagamkamalakar profile image
kamalakar

Thank you for this

Collapse
 
duranmla profile image
Alexis Duran

The video was awesome. I am glad you record it.
Awesome energy and super clear, thanks for sharing.

Congratz bro! 🚀

Collapse
 
ycmjason profile image
YCM Jason

Thank you!

Collapse
 
arnavkr profile image
Arnav Kumar

the way too awesome article I have ever seen about programming 😍

Collapse
 
ycmjason profile image
YCM Jason

Bless you too Linus!

Collapse
 
steven_slick_7e10cec9e642 profile image
Steven Slick

I've created a to-do app using this simple virtual DOM but I've got problems that the code does not handle deletion and reordering of real DOM element's