Author: deathmood

Translator: Front-end wisdom

Source: medium

Like it and see. Make it a habit


GitHub: github.com/qq449245884… Has included more categories of previous articles, as well as a lot of my documentation and tutorial material. Welcome Star and Perfect, you can refer to the examination points for review in the interview, I hope we can have something together.

Personal column ES6 has been launched, in-depth ES6, through case study to master some new features of ES6 using skills and principles, continuous update, ← click to subscribe.

In order to ensure readability, this paper adopts free translation rather than literal translation.

To build your own virtual DOM, you need to know two things. You don’t even need to go deep into the source code for React or any other virtual DOM implementation because they’re so large and complex — but in reality, the main part of the virtual DOM takes less than 50 lines of code.

There are two concepts:

  • The Virtual DOM is a mapping of the real DOM
  • When some nodes in the virtual DOM tree change, a new virtual tree is created. The algorithm compares the two trees (the new tree and the old tree), finds the differences, and then simply makes the corresponding changes in the real DOM.

Simulate DOM trees with JS objects

First, we need to store the DOM tree in memory somehow. You can do this using normal JS objects. Suppose we have a tree like this:

< ul class = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code

Seems simple enough, right? How do you represent it in JS objects?

{type: "ul", props: '} {' class ':' list are children: [{type: "li", props: {}, children: [' item 1]}, {type: 'li', props: {}, children: [' item 2 ']}]}Copy the code

There are two things to note here:

  • DOM elements are represented with the following objects

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

  • DOM text nodes are represented as plain JS strings

But representing a large Dom tree in this way is quite difficult. Let’s write an auxiliary function to make it easier to understand:

The function h (type, props,... children) { return { type, props, children }; }Copy the code

Rearrange the original code with this method:

H (' ul ', '} {' class ':' list are h (" li ", {}, "item 1"), h (" li ", {}, "item 2"),);Copy the code

It looks a lot cleaner, and you can take it a step further. Here JSX is used as follows:

The ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code

Compiled into:

The React. The createElement method (" ul ", {className: 'list'}, the React. CreateElement method (" li ", {}, "item 1"), the React. The createElement method (" li ", {}, "item 2"),);Copy the code

Does it look familiar? If we can use the h(…) we just defined. Function instead of react.createElement (…) , then we can also use JSX syntax. Instead, add a comment at the head of the source file:

/ / @ JSX h * * * < ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code

It actually tells Babel ‘Hey, dude help me compile JSX syntax with h(…). Function instead of react.createElement (…) “And Babel starts compiling. ‘

To sum up, we write DOM like this:

/ / @ JSX h * * * const a = (< ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >);Copy the code

Babel will help us compile code like this:

Const a = (h (' ul ', '} {the className: 'list are h (" li ", {}, "item 1"), h (" li ", {}, "item 2"),);) ;Copy the code

When the function “h” executes, it returns 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]}});Copy the code

Mapping from the Virtual DOM to the real DOM

Ok, so 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 declare some terms:

  • Using ‘$The variable starting with ‘represents a real DOM node (element, text node), so $parent will be a real DOM element
  • The virtual DOM uses a namenodeVariable representation of

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

So, write a function createElement(…) , which gets a virtual DOM node and returns a real DOM node. Ignoring the props and children attributes:

Function createElement(node) {if (Typeof Node === 'string') {return document.createTextNode(node); } return document.createElement(node.type); }Copy the code

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

{type: '... 'and props: {... }, children: [...] }Copy the code

Therefore, it is possible to pass in a virtual text node and a virtual element node in the function createElement.

Now let’s consider child nodes — each of which is a text node or element. So they can also use createElement(…) Function creation. Yes, this is like recursion, so we can call createElement(…) for each element’s children. AppendChild () then adds it 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; }Copy the code

Wow, that looks good. Put the node props property aside for a moment. Talk to you later. We don’t need them to understand the basic concepts of the virtual DOM, because they add complexity.

/** @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));Copy the code

Compare the differences between two virtual DOM trees

Now that we can convert the virtual DOM to the real DOM, we need to consider comparing the differences between the two DOM trees. Basically, we need an algorithm to compare the new tree to the old one, to let us know what has changed, and to change the real DOM accordingly.

How do YOU compare DOM trees? The following situations need to be addressed:

  • Add a new node using appendChild(…) Method to add a node

  • To remove old nodes, use removeChild(…) Method to remove the old node

  • Node replacement, using replaceChild(…) methods

If the nodes are the same — then you need to deep compare the child nodes

Write a file called updateElement(…) Function, which takes three arguments — ** $parent**, newNode, and oldNode, where $parent is the parent of an actual DOM element of the virtual node. Now take a look at how to handle all of the cases described above.

Adding a New Node

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

Removing an old node

There is a problem here — if there is no node in the current location of the new virtual tree — we should remove it from the actual DOM — how do we do this?

If we know the parent (passed as an argument), we can call $parent.removechild (…). Method maps the changes to the real DOM. $parent. ChildNodes [index] = $parent. ChildNodes [index];

Well, let’s assume that this index will be passed to the updateElement function (which it will — you’ll see later). The code is as follows:

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

Node replacement

First, you need to write a function that compares two nodes (the old node and the new node) and tells the node if it has really changed. Also consider whether the node can be an element or a text node:

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

Now that the current node has the index attribute, we can simply replace it with a 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] ); }}Copy the code

Comparison child node

Last, but not least — we should iterate over each child of these two nodes and compare them — we actually call **updateElement(…) for each node. ** method, also need to use recursion.

  • We only need to compare when the node is a DOM element (text nodes have no children)

  • We need to pass a reference to the current node as the parent node

  • We should compare all child nodes one by one, even if it is undefined, our function will handle it correctly.

  • And finally index, which is the index of the subarray’s subnode

    function updateElement(parent.appendChild( createElement(newNode) ); } else if (! newNode) {parent.childNodes[index] ); } else if (changed(newNode, oldNode)) { 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); });Copy the code

HTML

<button id="reload">RELOAD</button>
<div id="root"></div>
Copy the code

CSS

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

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

conclusion

We’ve now written the virtual DOM implementation and seen how it works. The authors hope that, after reading this article, they have some basic concepts for understanding how the virtual DOM works and how to respond behind the scenes.

However, here are a few things that are not highlighted (I’ll cover them in a future article):

  • Set element properties (props) and diffing/updating

  • Handle events – Add event listeners to elements

  • Make the virtual DOM work with components, such as React

  • Gets a reference to the actual DOM node

  • Use a virtual DOM with libraries that directly alter the real DOM, such as jQuery and its plug-ins


Original text: medium.com/@deathmood/…

The bugs that may exist after code deployment cannot be known in real time. In order to solve these bugs, I spent a lot of time on log debugging. Incidentally, HERE I recommend a good BUG monitoring tool Fundebug.


communication

Dry goods series of articles summarized as follows, feel good point Star, welcome to add groups to learn from each other.

Github.com/qq449245884…

Due to space constraints, today’s share only here. If you want to know more, you can scan the TWO-DIMENSIONAL code at the bottom of each article, and then follow our wechat public number to learn more information and valuable content.