DEV Community

loading...

Building a Simple Virtual DOM from Scratch

Jason Yu 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
Jason Yu Author

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
Jason Yu Author

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
Jason Yu Author

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

Collapse
ycmjason profile image
Jason Yu Author

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
virtualkirill 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
Jason Yu Author

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 😉

ycmjason profile image
Jason Yu Author • 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
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
Jason Yu Author

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
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
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
Jason Yu Author

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.

ycmjason profile image
Jason Yu Author • 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.

ycmjason profile image
Jason Yu Author • 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
Jason Yu Author

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

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
Jason Yu Author

❤❤❤ thank you so much!!!!

Collapse
ycmjason profile image
Jason Yu Author

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

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
Jason Yu Author

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
Jason Yu Author

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
Jason Yu Author

Just feel free to DM me on twitter. @ycmjason

Collapse
arnavkr profile image
Arnav Kumar

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

Collapse
lednhatkhanh profile image
Nhat Khanh

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

Collapse
schneiderfelipe profile image
Felipe S. S. Schneider

Hey, I implemented a virtual DOM in PureScript mostly based on your article! github.com/schneiderfelipe/purescr...

Thank you for making content like this!

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
Jason Yu Author

Thank you!

Collapse
ycmjason profile image
Jason Yu Author

Bless you too Linus!