4
Author: deathmood
Translator: Frontend Xiaozhi
Source: medium
There are dreams, dry goods, WeChat search [Da Qian World] Pay attention to this Shuawanzhi who is still doing dishes in the early morning.
This article GitHub https://github.com/qq449245884/xiaozhi has been included, the first-line interview complete test site, information and my series of articles.

To build your own virtual DOM, you need to know two things. You don't even need to dive into the source code of React or the source code of any other virtual DOM implementation, because they are so large and complex-but in reality, the main part of the virtual DOM only requires less than 50 lines of code.

There are two concepts:

  • Virtual DOM is a mapping of real DOM
  • When some nodes in the virtual DOM tree are changed, a new virtual tree will be obtained. The algorithm compares the two trees (new tree and old tree) to find the difference, and then only needs to make corresponding changes on the real DOM.

Simulate the DOM tree with JS objects

First, we need to store the DOM tree in memory in some way. It can be done using ordinary JS objects. Suppose we have such a tree:

<ul class=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

It seems very simple, right? How to use JS objects to represent it?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
  { type: ‘li’, props: {}, children: [‘item 1’] },
  { type: ‘li’, props: {}, children: [‘item 2’] }
] }

There are two things to note here:

  • Use the following objects to represent DOM elements
{ type: ‘…’, props: { … }, children: [ … ] }

  • Use ordinary JS strings to represent DOM text nodes

However, it is quite difficult to represent a Dom tree with a lot of content in this way. Here is a helper function to make it easier to understand:

function h(type, props, …children) {
  return { type, props, children };
}

Use this method to rearrange the initial code:

h(‘ul’, { ‘class’: ‘list’ },
  h(‘li’, {}, ‘item 1’),
  h(‘li’, {}, ‘item 2’),
);

This looks much more concise, and it can go a step further. JSX is used here, as follows:

<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

Compiled into:

React.createElement(‘ul’, { className: ‘list’ },
  React.createElement(‘li’, {}, ‘item 1’),
  React.createElement(‘li’, {}, ‘item 2’),
);

Does it look familiar? If we can use the h(...) function we just defined instead of React.createElement(…) , then we can also use JSX syntax. In fact, you only need to add such a comment to the head of the source file:

/** @jsx h */
<ul className=”list”>
  <li>item 1</li>
  <li>item 2</li>
</ul>

It actually tells Babel 'Hey, little brother to help me compile JSX grammar, with h(...) instead of function React.createElement(…) , then Babel began to compile. '

In summary, we write the DOM as follows:

/** @jsx h */
const a = (
  <ul className=”list”>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

Babel will help us compile this code:

const a = (
  h(‘ul’, { className: ‘list’ },
    h(‘li’, {}, ‘item 1’),
    h(‘li’, {}, ‘item 2’),
  );
);

When the function “h” executed, it will return a normal JS object-our virtual DOM:

const a = (
  { type: ‘ul’, props: { className: ‘list’ }, children: [
    { type: ‘li’, props: {}, children: [‘item 1’] },
    { type: ‘li’, props: {}, children: [‘item 2’] }
  ] }
);

Mapping from Virtual DOM to real DOM

Okay, now we have a DOM tree, represented by ordinary JS objects, and our own structure. This is cool, but we need to create a real DOM from it.

First let's make some assumptions and state some terms:

  • Use the variable starting with ' $ ' to represent the real DOM node (element, text node), so $parent will be a real DOM element
  • The virtual DOM is node by a variable named 061c90266e2c61

* Just like in React, there can only be one root node-all other nodes are in it

So, to write a function createElement(…) , it will get a virtual DOM node and return a real DOM node. Here do not consider the props and children attributes:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  return document.createElement(node.type);
}

In the above method, I can also create two types of nodes, text nodes and Dom element nodes, which are JS objects of type:

{ type: ‘…’, props: { … }, children: [ … ] }

Therefore, you can pass in virtual text nodes and virtual element nodes createElement

Now let us consider child nodes-each of them is a text node or element. So they can also be createElement(...) function. Yes, it's like recursion, so we can call createElement(...) for the child elements of each element, and then use appendChild() add to our element:

function createElement(node) {
  if (typeof node === ‘string’) {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

Wow, it looks good. First put the node props attribute aside. Talk about it later. We don't need them to understand the basic concepts of virtual DOM, because they add complexity.

The complete code is as follows:

/** @jsx h */

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const $root = document.getElementById('root');
$root.appendChild(createElement(a));

Compare the differences between two virtual DOM trees

Now we can convert the virtual DOM to the real DOM, which requires consideration of the differences between the two DOM trees. Basically, we need an algorithm to compare the new tree with the old tree. It can let us know what has changed, and then change the real DOM accordingly.

How to compare DOM trees? Need to deal with the following situations:

  • To add a new node, use the appendChild(...) method to add the node

图片描述

  • To remove the old node, use the removeChild(...) method to remove the old node

图片描述

  • To replace the node, use the replaceChild(...) method

图片描述

If the nodes are the same-you need to compare the child nodes in depth

图片描述

Write a function named updateElement (...) function, which accepts three parameters - $parent , newNode and oldNode , which $ parent is the parent element of an actual DOM element of the virtual node. Now let's see how to deal with all the situations described above.

Add new node

function updateElement($parent, newNode, oldNode) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  }
}

Remove old node

There is a problem here-if there is no node at the current position of the new virtual tree-we should delete it from the actual DOM-how can this be done?

If we know the parent element (passed by parameter), we can call the $parent.removeChild(…) method to map the change to the real DOM. But only if we know the index of our node on the parent element, we can get the reference of the node $parent.childNodes[index]

Okay, let's assume that this index will be passed to the updateElement function (it will indeed be passed-we will see later). code show as below:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  }
}

Node replacement

First, you need to write a function to compare two nodes (the old node and the new node) and tell whether the node has really changed. There is also need to consider that this node can be an element or a text node:

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === ‘string’ && node1 !== node2 ||
         node1.type !== node2.type
}

Now that the current node has the index attribute, you can easily replace it with the new node:

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  }
}

Compare child nodes

Last, but not least—we should traverse every child node of these two nodes and compare them—actually call the updateElement(...) method for each node, which also requires recursion.

  • We only need to compare when the node is a DOM element (the text node has no children)
  • We need to pass the reference of the current node as the parent node
  • We should compare all child nodes one by one, even if it is undefined it doesn't matter, our function will handle it correctly.
  • The last is index , which is the index of the child node in the sub-array
function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } 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
      );
    }
  }
}

Complete code

Babel+JSX
/* @jsx h /

function h(type, props, ...children) {
  return { type, props, children };
}

function createElement(node) {
  if (typeof node === 'string') {
    return document.createTextNode(node);
  }
  const $el = document.createElement(node.type);
  node.children
    .map(createElement)
    .forEach($el.appendChild.bind($el));
  return $el;
}

function changed(node1, node2) {
  return typeof node1 !== typeof node2 ||
         typeof node1 === 'string' && node1 !== node2 ||
         node1.type !== node2.type
}

function updateElement($parent, newNode, oldNode, index = 0) {
  if (!oldNode) {
    $parent.appendChild(
      createElement(newNode)
    );
  } else if (!newNode) {
    $parent.removeChild(
      $parent.childNodes[index]
    );
  } else if (changed(newNode, oldNode)) {
    $parent.replaceChild(
      createElement(newNode),
      $parent.childNodes[index]
    );
  } 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
      );
    }
  }
}

// ---------------------------------------------------------------------

const a = (
  <ul>
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

const b = (
  <ul>
    <li>item 1</li>
    <li>hello!</li>
  </ul>
);

const $root = document.getElementById('root');
const $reload = document.getElementById('reload');

updateElement($root, a);
$reload.addEventListener('click', () => {
  updateElement($root, b, a);
});

HTML

<button id="reload">RELOAD</button>
<div id="root"></div>

CSS

#root {
  border: 1px solid black;
  padding: 10px;
  margin: 30px 0 0 0;
}

Open the developer tools and observe the changes applied when the "Reload" button is pressed.

图片描述

Summarize

Now we have written the virtual DOM implementation and understand how it works. The author hopes that after reading this article, he has a certain understanding of the basic concepts of how the virtual DOM works and how to respond behind the scenes.

However, there are some things that are not highlighted here (we will cover them in a future article):

  • Set element properties (props) and diffing/updating
  • Handling events-adding event listeners to the element
  • Let virtual DOM work with components, such as React
  • Get a reference to the actual DOM node
  • Use virtual DOM with libraries, these libraries can directly change the real DOM, such as jQuery and its plug-ins

Original:
https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

code is deployed, the possible bugs cannot be known in real time. In order to solve these bugs afterwards, a lot of time was spent on log debugging. By the way, I would like to recommend a useful BUG monitoring tool Fundebug .


comminicate

If you have dreams and dry goods, search on [Move to the World] attention to this wise brush who is still doing dishes in the early morning.

This article GitHub https://github.com/qq449245884/xiaozhi has been included, the first-line interview complete test site, information and my series of articles.


王大冶
68k 声望104.9k 粉丝