DEV Community

Cover image for Explained and created a simple virtual DOM from scratch
Erick Sosa Garcia
Erick Sosa Garcia

Posted on • Updated on

Explained and created a simple virtual DOM from scratch

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.

DOM tree

More information

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']
};
Enter fullscreen mode Exit fullscreen mode

html equivalent


  <button id="btn" class="save-btn">save</button>

Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
// 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'));

Enter fullscreen mode Exit fullscreen mode

run this on any web server, i run it with live server in vscode

Alt Text

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;
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

run it 🤞

gif

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

Top comments (1)

Collapse
 
perokhel profile image
perokhel • Edited

How are events included in this implementation of a customized virtual DOM?