How to write your own Virtual DOM
This article is reprinted from: Zhongcheng Translator: Yanni4night link: www.zcfy.cc/article/113… Original text: medium.com/@deathmood/…
To build your own Virtual DOM, there are only two things you need to know. You don’t even have to dig into the source code for React or any other Virtual DOM implementation. They are too large and complex — but the reality is that the main parts of the Virtual DOM can be implemented in less than 50 lines of code. 50 lines!!
Two concepts:
- The Virtual DOM is any representation of the real DOM.
- Changes in the Virtual DOM tree create a new Virtual DOM tree. Algorithms that compare old and new Virtual DOM trees calculate differences and make minimal changes to the real DOM, called “Virtual”
That’s it. Let’s dig deep into the meaning of each concept.
Update: The second article on setting properties and events in the Virtual DOM is here.
Describe the DOM tree
First, we need to store the DOM tree in memory somehow. It can be implemented using pure JavaScript objects. Suppose we had a tree like this:
< ul class = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code
Seems pretty simple, right? How do we 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
Here we emphasize two things:
- We represent DOM elements as objects
{type: '... 'and props: {... }, children: [...] }Copy the code
- We represent the text nodes of the DOM as pure JS strings
But writing large trees in this way is very difficult. So let’s write a helper function to make it easier to understand the structure:
The function h (type, props,... children) { return { type, props, children }; }Copy the code
Now writing data to the tree looks like this:
H (' ul ', '} {' class ':' list are h (" li ", {}, "item 1"), h (" li ", {}, "item 2"),);Copy the code
Looks a lot clearer, doesn’t it? Let’s go one step further. You’ve heard of JSX, right? Well, I’m going to implement that, too. So how does it work?
If you’ve read Babel’s official JSX documentation, you’ll know that Babel puts the following code:
The ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code
Translated 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
Notice the similarities? Yeah, yeah, yeah, if we put react.createElement (…) Replace it with our h(…) That’s good — we can actually do this using what are called JSX compile instructions. Just put something like a comment at the beginning of the source:
/ / @ JSX h * * * < ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >Copy the code
This line actually tells Babel: Hey, compile JSX with H instead of react. createElement. You can replace h with anything, and it will compile.
So, to summarize what I said above, we’ll write DOM as follows:
/ / @ JSX h * * * const a = (< ul className = "list" > < li > item 1 < / li > < li > item 2 < / li > < / ul >);Copy the code
Babel would translate it as:
Const a = (h (' ul ', '} {the className: 'list are h (" li ", {}, "item 1"), h (" li ", {}, "item 2"),);) ;Copy the code
When the function h is executed, it returns a pure JS object — our Virtual DOM representation:
Const a = ({type: 'ul', props: {className: 'list'}, children: [{type: 'li', props: {}, children: "Item 1"}, {type: "li", props: {}, children: [' item 2]}});Copy the code
JSFiddle
Apply the DOM representation
Ok, now we have pure JS objects and a DOM tree representation of our own structure. That’s cool, but we need to use it to create a real DOM. After all, we can’t write expressions directly into the DOM.
Let’s start with a series of assumptions and some terminology:
- I will use
$
The variables at the beginning represent the actual DOM nodes (elements and text), so$parent Is a real DOM element; - The Virtual DOM representation is stored in the variable node;
- Like React, you can have only one root node — everything else is its descendants
Ok, as mentioned earlier, we write a function createElement(…) Convert virtual DOM nodes into real DOM nodes. Forget props and children for a moment — later:
Function createElement(node) {if (Typeof Node === 'string') {return document.createTextNode(node); } return document.createElement(node.type); }Copy the code
Because we already have text nodes represented as pure JS strings and elements represented as JS objects like the following:
{type: '... 'and props: {... }, children: [...] }Copy the code
Therefore, we can deal with both virtual text nodes and virtual element nodes here.
Now consider children — each of which is either a text node or an element. So they can all use our createElement(…) Function to create. Ah… Do you feel it? I feel recursion :)) so we call createElement(…) on each element of children. Add our element with appendChild() like this:
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, it looks great. Let’s hold off on the props and discuss it later, because they are not needed to understand basic Virtual DOM concepts and only add complexity.
With updates
Ok, now that we can convert the virtual DOM to the real DOM, it’s time to compare the virtual tree differences. Basically, we write an algorithm that compares the differences between the old and new trees and makes the least necessary updates to the real DOM.
How do you compare trees? We need to address the following issues:
- There is a new node somewhere — so the node is added, we need appendChild(…) It;
- There is an old node somewhere — so the node is removed, we need removeChild(…) It;
- There are different nodes somewhere — nodes are updated, we need replaceChild(… It;
- The nodes are the same, and I need to go down to the next level and compare the child nodes
Ok, let’s write a functionUpdateElement (…)., enter 3 parameters,parentIs the parent of the real node corresponding to our virtual node. Now let’s see how we can deal with the problems mentioned above.
I have a new node
It’s so simple that you don’t even need to comment:
function updateElement($parent, newNode, oldNode) {
if (!oldNode) {
$parent.appendChild(
createElement(newNode)
);
}
}Copy the code
The old node
There is a problem here — if there is no node in the current location of the Virtual DOM tree — we should remove it from the real DOM tree — but how do we do that? Yes, we know the parent element, so we should callparent.childNodes[index]Get the reference, hereindexIs the index:
Suppose the index is passed into our function (as we’ll see later, it is). So our code is:
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 updates
First we need to write a function that compares two nodes (new and old) and tells us whether the node has really been updated. We should consider elements and text nodes:
function changed(node1, node2) { return typeof node1 ! = = typeof 2 | | typeof node1 = = = 'string' && node1! == node2 || node1.type ! == node2.type }Copy the code
Now, with index, we can easily 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
The last and most important point — we should iterate over the nodes on both sides and compare them — is actually to call updateElement(…) in turn. . Yeah, recursion again.
Before writing code, there are a few things to consider:
- We only compare children of elements (text has no children);
- Now we have a reference to the current node as the parent node;
- We should compare all child nodes one by one — even if encountered
undefined
It doesn’t matter, our function can handle it; - The lastindex— It’s just the child node in
children
The index
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
);
}
}
}Copy the code
comprehensive
Well, we’re done. I put all of my code into JSFiddle, and the implementation actually uses 50 lines of code, as I promised you. Go play with it.
Open the developer tools and watch the app update after you hit the Reload button.
conclusion
Congratulations to you! We achieved our goal, implemented our own Virtual DOM, and it worked. I hope by the end of this article you have a basic understanding of how Virtual DOM works and the inner workings of React.
However, there are some things we didn’t emphasize here (which I’ll cover in a future article) :
- Sets element attributes and compares or updates them;
- Handle events – add events to elements
- Make Virtual DOM work with components, like React;
- Get a reference to a real DOM node.
- Make Virtual DOM work with libraries that manipulate the DOM directly, such as jQuery and plug-ins;
- The other…
P.S.
If there are any errors in the code or text, or any optimizations the code can have, please point them out in the comments 🙂 also, I apologize for my English 🙂
Update: The second article on setting properties and events in the Virtual DOM is here.