Web
Implementing Virtual DOM with Vanilla JavaScript

Implementing Virtual DOM with Vanilla JavaScript

Source: View full code (opens in a new tab)

📚 Summary for implementing Virtual DOM 📚

  • React Element
  • JSX vs JS
  • Virtual DOM

React Element

  • The smallest unit of a React app.
  • A component of a component.
  • Different from browser DOM element.
    • The most general-purpose base class that all objects in Document inherit.
    • Only has common methods and properties.
    • A class that expresses a specific element in more detail inherits element. ex. HTMLElement interface: The base interface for HTML elements. ex. SVGElement interface: The basis for all SVG elements.

Root DOM Node

  • Applications implemented in React typically have one root DOM node.
  • All elements contained herein are managed by React Dom.
<div id="root"></div>
  • To render a React element to the root DOM node, pass both to ReactDOM.render().
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById("root"));

JSX vs JS

  • JSX: A grammar that extends JavaScript. Create a React element.
  • Each JSX element uses Syntax Sugar to call React.createElement(component, props, ...children).
  • The JSX syntax itself cannot be read by the browser. Must be converted to plain JavaScript.
  • Babel (opens in a new tab) : Converting JSX to JavaScript that calls the React.createElement function.
// JSX
class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
 
ReactDOM.render(<Hello toWhat="World" />, document.getElementById("root"));
// JS
class Hello extends React.Component {
  render() {
    return React.createElement("div", null, `Hello ${this.props.toWhat}`);
  }
}
 
ReactDOM.render(
  React.createElement(Hello, { toWhat: "World" }, null),
  document.getElementById("root")
);
<ul className="list">
  <li>item 1</li>
  <li>item 2</li>
</ul>;
 
// If you look at the Babel JSX document, Babel translates the above code as follows:
 
React.createElement(
  "ul",
  { className: "list" },
  React.createElement("li", {}, "item 1"),
  React.createElement("li", {}, "item 2")
);

Virtual DOM (VDOM)

  • A chunk of objects modeled after the DOM form.

1.Represent DOM Tree.

  • Saving the DOM Tree in memory as a JavaScript Object (Virtual DOM).
// For example, suppose you implemented the Tree below:
<ul class="list">
  <li>item 1</li>
  <li>item 2</li>
</ul>
 
// If the above DOM element is expressed as a JS Object, it is as follows.
{
  type: 'ul', props: { 'class': 'list' }, children: [
    { type: 'li', props: {}, children: ['item 1'] },
    { type: 'li', props: {}, children: ['item 2'] }
  ]
}
// Repeat type, props, children. -> It is difficult to create a large tree.
// Use after creating a helper function
function h(type, props, ...children) {
  return { type, props, children: children.flat() };
}
 
h("ul", { class: "list" }, h("li", {}, "item 1"), h("li", {}, "item 2"));
// It looks similar to JSX?!
 
// Let’s replace React.createElement with the h function through jsx pragma
// pragma: Compiler directive. Tells the compiler how to process the contents of a file.
// 1. Add options to Babel plugin. 2. Setting pragma comment at the beginning of module.
// jsx pragma. Tell Babel to use h instead of React.createElement
/** @jsx h */
 
const a = (
  <ul className="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);
 
// Babel converts the above code as follows:
 
const a = (
  h('ul', { className: 'list' },
    h('li', {}, 'item 1'),
    h('li', {}, 'item 2'),
  );
);
 
// When you run the h function (a helper function you created yourself), a JavaScript Object (Virtual DOM representation) is returned.
 
const a = (
  { type: 'ul', props: { className: 'list' }, children: [
    { type: 'li', props: {}, children: ['item 1'] },
    { type: 'li', props: {}, children: ['item 2'] }
  ] }
);

2. Expressing Real DOM as JavaScript Object (Virtual DOM)

  • Changing Virtual DOM to Real DOM.

  • 3 rules in the code below.

    • All variables are written as Real DOM (element, text node) starting with “$”.
    • Virtual DOM representation is included in the node variable.
    • Has only one root node. All other nodes are located within the root node.
    • Excluding props. (Properties are not necessary to understand the basic concept of Virtual DOM)
// createElement function: Gets a Virtual DOM Node and returns a Real DOM Node
function createElement(node) {
  if (typeof node === "string") {
    // TextNode (JavaScript string)
    return document.createTextNode(node);
  }
 
  // { type: '… ', props: { … }, children: [ … ] } JavaScript Object of type
  const $el = document.createElement(node.type);
  node.children.map(createElement).forEach($el.appendChild.bind($el));
  return $el;
}

3. Process changes

  • Detection of changes in Virtual Tree.
  • When there is a change in the view (HTML), the old virtual dome (Old Node) and the new virtual dome (New Node) are compared and only the changes are applied to the DOM.
function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}
 
 
// updateElement function: receives parent, newNode, oldNode, index parameters
// parent: Real DOM element parent of Virtual Node
// index: location of the node in the parent element
function updateElement($parent, newNode, oldNode, index = 0) {
 
  // When newNode exists but oldNode does not exist (when a new node is added)
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
 
 
  // When oldNode exists but newNode does not exist (when the node must be deleted)
  } else if (!newNode) {
      $parent.removeChild(
        $parent.childNodes[index]
      );
    }
 
  // If oldNode and newNode are different (node ​​has changed)
  // Replace newNode (newly created node) from $parent (parent node) to index (index of current node)
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
 
 
  // Since newNode and oldNode are the same, compare all children of the two nodes
  // Actually, updateElement must be called on each node (recursive)
  // Considerations
  // - Since text nodes cannot have children, only element nodes are compared.
  // - Pass a reference to the current node to the parent
  // - Compare all children one by one
  // - Index (i): Index of child node in children array
  } else if (newNode.type) {
      const newLength = newNode.children.length;
      const oldLength = oldNode.children.length;
      for (let i = 0; i < newLength || i < oldLength; i++) {
        updateElement(
          $parent.childNodes[index],
          newNode.children[i],
          oldNode.children[i],
          i
        );
      }
    }
}