As we all know, direct DOM manipulation is a performance-consuming task for the front-end. Many frameworks represented by React and Vue generally adopt Virtual DOM to solve the performance problem of frequently updating DOM caused by frequent state changes in increasingly complex Web applications. In this paper, the author realized a very simple Virtual DOM through practical operation, deepening the understanding of Virtual DOM in the mainstream front-end framework today.
Virtual DOM, the community has many excellent articles, and this article is the author to use their own way, and draw on the implementation of predecessors, in a simple way, Virtual DOM simple implementation, but does not contain snabbDOM source analysis, in the author’s final implementation, The Virtual DOM implementation of this paper is improved by referring to the principle of SNabbDOM. Interested readers can read the above articles and refer to the final code of this paper.
This article should take about 15 to 20 minutes to read.
An overview of the
This article is divided into the following aspects to describe the minimalist version of Virtual DOM core implementation:
- The main idea of Virtual DOM
- DOM trees are represented with JavaScript objects
- Convert the Virtual DOM to the real DOM
- Set the node type
- Set the properties of a node
- Processing of child nodes
- Dealing with change
- Nodes are added or deleted
- Update the node
- Update child nodes
The main idea of Virtual DOM
To understand what Virtual DOM means, you need to understand the DOM. DOM is an API for HTML and XML documents. DOM represents a hierarchical tree of nodes. The Virtual DOM is described abstractly by JavaScript objects. The Virtual DOM is essentially a JavaScript object, and you can map a Virtual DOM tree to a real DOM tree using the Render function.
Once the Virtual DOM is changed, a new Virtual DOM is generated, and the relevant algorithm compares the old and new Virtual DOM trees, finds the differences between them, and updates the real DOM tree with as little DOM manipulation as possible.
We can express the relationship between Virtual DOM and DOM as: DOM = Render(Virtual DOM).
DOM trees are represented with JavaScript objects
The Virtual DOM is represented as JavaScript objects and stored in memory. JSX will eventually be compiled by Babel into JavaScript objects used to represent the Virtual DOM. Consider the following JSX:
<div>
<span className="item">item</span>
<input disabled={true} / ></div>
Copy the code
This will eventually be compiled by Babel into the following JavaScript object:
{
type: 'div',
props: null,
children: [{
type: 'span',
props: {
class: 'item',
},
children: ['item'],}, {type: 'input',
props: {
disabled: true,
},
children: [],
}],
}
Copy the code
Two things can be noted:
- All DOM nodes are objects that look like this:
{ type: '... ', props: { ... }, children: { ... }, on: { ... }}Copy the code
- The nodes in this article are represented as JavaScript strings
So how does JSX translate into JavaScript objects? Fortunately, there are many excellent tools in the community to help us do this, and due to lack of space, we won’t discuss this issue in this article. In order to facilitate you to understand the Virtual DOM more quickly, for this step, the author used open source tools to complete. The famous Babel plugin, babel-plugin-transform-react-jsx, helps us do this.
To better use babel-plugin-transform-react-jsX, we need to set up a WebPack development environment. The specific process is not described here. Students who are interested in their own implementation can go to the simple-virtual-dom to view the code.
Create a Virtual DOM using the babel-plugin-transform-react-jsx function:
function vdom(type, props, ... children) {
return {
type,
props,
children,
};
}
Copy the code
We can then create our Virtual DOM tree with the following code:
const vNode = vdom('div'.null,
vdom('span', { class: 'item' }, 'item'),
vdom('input', { disabled: true}));Copy the code
Enter the above code on the console, and you can see that you have created a Virtual DOM tree represented by JavaScript objects:
Convert the Virtual DOM to the real DOM
Now that we know how to use JavaScript objects to represent our real DOM tree, how does the Virtual DOM translate into the real DOM for us?
Before we get there, there are a few caveats:
- In the code, I will use the
$
The opening variable represents the real DOM object; toRealDom
The function takes a Virtual DOM object as an argument and returns a real DOM object.mount
The function takes two arguments: it will mount the parent node of the Virtual DOM object, a real DOM object named$parent
; And the mounted Virtual DOM objectvNode
;
Here is a prototype toRealDom function:
function toRealDom(vNode) {
let $dom;
// do something with vNode
return $dom;
}
Copy the code
We can convert a vNode object into a real DOM object using the toRealDom method, and mount the real DOM via appendChild:
function mount($parent, vNode) {
return $parent.appendChild(toRealDom(vNode));
}
Copy the code
Let’s deal with the vNode types, props, and children, respectively.
Set the node type
First, since we have both a text node of character type and an Element node of object type, we need to do a separate processing for type:
if (typeof vNode === 'string') {
$dom = document.createTextNode(vNode);
} else {
$dom = document.createElement(vNode.type);
}
Copy the code
In such a simple toRealDom function, the processing of Type is complete, so let’s look at the processing of props.
Set the properties of a node
We know that if a node has props, then props is an object. Each class of props is processed separately by traversing the props and calling the setProp method.
if (vNode.props) {
Object.keys(vNode.props).forEach(key= > {
setProp($dom, key, vNode.props[key]);
});
}
Copy the code
SetProp accepts three arguments:
$target
This is a real DOM object,setProp
DOM manipulation will be performed on this node;name
Is the attribute name;value
Represents the value of the attribute;
Now that you’ve read this, you have a general idea of what setProp needs to do. In general, for ordinary props, we attach attributes to DOM objects using setAttributes.
function setProp($target, name, value) {
return $target.setAttribute(name, value);
}
Copy the code
But that’s not enough. Consider the following JSX structure:
<div>
<span className="item" data-node="item" onClick={() => console.log('item')}>item</span>
<input disabled={true} />
</div>
Copy the code
From the JSX structure above, we notice the following:
- Due to the
class
Is a JavaScript reserved word that JSX generally usesclassName
To represent the DOM nodeclass
; - General with
on
The leading property to represent the event; - In addition to character types, attributes may also be Boolean values, such as
disabled
, when the value istrue
, add this attribute.
Therefore, setProp also needs to consider the above situation:
function isEventProp(name) {
return /^on/.test(name);
}
function extractEventName(name) {
return name.slice(2).toLowerCase();
}
function setProp($target, name, value) {
if (name === 'className') { // Since class is a reserved word, JSX uses className to represent a node's class
return $target.setAttribute('class', value);
} else if (isEventProp(name)) { // For attributes that start with on, are events
return $target.addEventListener(extractEventName(name), value);
} else if (typeof value === 'boolean') { // The compatibility attribute is Boolean
if (value) {
$target.setAttribute(name, value);
}
return $target[name] = value;
} else {
return$target.setAttribute(name, value); }}Copy the code
Finally, there is a class of properties that are our custom properties. For example, the state transfer between components in the mainstream framework is passed through props. We don’t want this class of properties to be displayed in the DOM, so we need to write a function isCustomProp to check whether this property is a custom property. Since this article is only intended to implement the core idea of the Virtual DOM, for convenience, this function returns false directly in this article.
function isCustomProp(name) {
return false;
}
Copy the code
The final setProp function:
function setProp($target, name, value) {
if (isCustomProp(name)) {
return;
} else if (name === 'className') { // fix react className
return $target.setAttribute('class', value);
} else if (isEventProp(name)) {
return $target.addEventListener(extractEventName(name), value);
} else if (typeof value === 'boolean') {
if (value) {
$target.setAttribute(name, value);
}
return $target[name] = value;
} else {
return$target.setAttribute(name, value); }}Copy the code
Processing of child nodes
For each item in children, it is a vNode object. When the Virtual DOM is converted into the real DOM, the child node also needs to be recursively transformed. It can be imagined that, for the case of child nodes, toRealDom needs to be recursively called on the child node, as shown in the following code:
if (vNode.children && vNode.children.length) {
vNode.children.forEach(childVdom= > {
const realChildDom = toRealDom(childVdom);
$dom.appendChild(realChildDom);
});
}
Copy the code
The final toRealDom looks like this:
function toRealDom(vNode) {
let $dom;
if (typeof vNode === 'string') {
$dom = document.createTextNode(vNode);
} else {
$dom = document.createElement(vNode.type);
}
if (vNode.props) {
Object.keys(vNode.props).forEach(key= > {
setProp($dom, key, vNode.props[key]);
});
}
if (vNode.children && vNode.children.length) {
vNode.children.forEach(childVdom= > {
const realChildDom = toRealDom(childVdom);
$dom.appendChild(realChildDom);
});
}
return $dom;
}
Copy the code
Dealing with change
Virtual DOM was created for the fundamental reason of performance improvement. Through Virtual DOM, developers can reduce many unnecessary DOM operations to achieve optimal performance. So let’s see how the Virtual DOM algorithm achieves performance optimization by comparing the Virtual DOM tree before and after the update.
Note: This paper is the simplest implementation of the author, currently the community is generally used for the algorithm is SNabbDOM, such as Vue is a Virtual DOM based on the algorithm, interested readers can view the source code of this library, based on the Virtual DOM small example of this paper, the author finally referred to the implementation of the algorithm. This article demo portal, due to space is limited, interested readers can study.
To handle the change, we first declare an updateDom function that takes the following four parameters:
$parent
Is the parent node to be mounted.oldVNode
And the oldVNode
Object;newVNode
The newVNode
Object;index
Is used when updating child nodes. Indicates the number of child nodes that are being updated. The default value is 0.
The function prototype is as follows:
function updateDom($parent, oldVNode, newVNode, index = 0) {}Copy the code
Nodes are added or deleted
Add a new node to the DOM tree using appendChild () :
Translated into code expressed as:
// There is no old node, add a new node
if(! oldVNode) {return $parent.appendChild(toRealDom(newVNode));
}
Copy the code
Similarly, to remove an old node, we use removeChild. In this case, we should remove the old node from the real DOM, but the problem is that we can’t get the node directly from this function. We need to know where the node is in the parent node. $parent. ChildNodes [index] = $parent. ChildNodes [index] = $parent.
Translated into code expressed as:
const $currentDom = $parent.childNodes[index];
// If there is no new node, delete the old node
if(! newVNode) {return $parent.removeChild($currentDom);
}
Copy the code
Update the node
The core of the Virtual DOM is how to update nodes efficiently. Let’s take a look at updating nodes.
If the text node is the same as the new one, we don’t need to update the DOM. In updateDom, we can simply return:
// All text nodes are unchanged
if (typeof oldVNode === 'string' && typeof newVNode === 'string' && oldVNode === newVNode) {
return;
}
Copy the code
Next, consider whether the node really needs to be updated. As shown in the figure, the type of a node has changed from SPAN to div, which obviously requires us to update the DOM:
We need to write a function isNodeChanged to help us determine if the old node and the new node are really the same, and if they are not, we need to replace the node:
function isNodeChanged(oldVNode, newVNode) {
// One is a textNode and one is an element
if (typeofoldVNode ! = =typeof newVNode) {
return true;
}
// Both textNodes compare whether the text has changed
if (typeof oldVNode === 'string' && typeof newVNode === 'string') {
returnoldVNode ! == newVNode; }// Both element nodes compare whether the node type has changed
if (typeof oldVNode === 'object' && typeof newVNode === 'object') {
return oldVNode.type !== newVNode.type;
}
}
Copy the code
In updateDom, if the node type changes, the node is replaced directly, as shown in the following code. The old DOM node is removed and the new DOM node is added by calling replaceChild:
if (isNodeChanged(oldVNode, newVNode)) {
return $parent.replaceChild(toRealDom(newVNode), $currentDom);
}
Copy the code
But that’s far from the end. Consider this:
<! -- old -->
<div class="item" data-item="old-item"></div>
Copy the code
<! -- new -->
<div id="item" data-item="new-item"></div>
Copy the code
Vnode. type is both ‘div’, but the attributes of the node have changed. In addition to updating the DOM for the change of node type, the DOM for the change of node attributes also needs to be updated accordingly.
Similarly, we write an isPropsChanged function to determine whether the attributes of the old and new nodes have changed:
function isPropsChanged(oldProps, newProps) {
// The props must have changed
if (typeofoldProps ! = =typeof newProps) {
return true;
}
// props is the object
if (typeof oldProps === 'object' && typeof newProps === 'object') {
const oldKeys = Object.keys(oldProps);
const newkeys = Object.keys(newProps);
// The number of props is different
if(oldKeys.length ! == newkeys.length) {return true;
}
// If the number of props is the same, check whether there are any inconsistent props
for (let i = 0; i < oldKeys.length; i++) {
const key = oldKeys[i]
if(oldProps[key] ! == newProps[key]) {return true; }}// The default is unchanged
return false;
}
return false;
}
Copy the code
IsPropsChanged checks whether the props of the old node and the props of the new node are of the same type, and whether the old node has the same props and the new node has a new attribute, or vice versa: The props of the new node is null, and the properties of the old node are deleted. If the type is inconsistent, the attribute must be updated.
Then, considering that the node has props before and after the update, we need to judge whether the props before and after the update are consistent, that is, whether the two objects are congruent, and we can traverse it. If the properties are not equal, the props changes, and the changes need to be handled.
Now, let’s go back to our updateDom function and see that the update to the Virtual DOM node props is applied to the real DOM.
// The type of the virtual DOM is unchanged
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
if (isPropsChanged(oldProps, newProps)) {
const oldPropsKeys = Object.keys(oldProps);
const newPropsKeys = Object.keys(newProps);
// If the new node has no attributes, remove the attributes of the old node
if (newPropsKeys.length === 0) {
oldPropsKeys.forEach(propKey= > {
removeProp($currentDom, propKey, oldProps[propKey]);
});
} else {
// Get all props to traverse, adding/deleting/modifying properties
const allPropsKeys = new Set([...oldPropsKeys, ... newPropsKeys]);
allPropsKeys.forEach(propKey= > {
// Attributes are removed
if(! newProps[propKey]) {return removeProp($currentDom, propKey, oldProps[propKey]);
}
// Attributes changed/increased
if(newProps[propKey] ! == oldProps[propKey]) {returnsetProp($currentDom, propKey, newProps[propKey]); }}); }}Copy the code
The code above is also very easy to understand, if you find that the props have changed, then traverse each item of the old props. Remove non-existent attributes and add the new attributes to the updated DOM tree:
- First, if the new node has no attributes, traverse removes all the attributes of the old node, in this case, by calling
removeProp
Delete it.removeProp
withsetProp
Correspondingly, due to the limited space of this article, THE author will not elaborate too much here;
function removeProp($target, name, value) {
if (isCustomProp(name)) {
return;
} else if (name === 'className') { // fix react className
return $target.removeAttribute('class');
} else if (isEventProp(name)) {
return $target.removeEventListener(extractEventName(name), value);
} else if (typeof value === 'boolean') {
$target.removeAttribute(name);
$target[name] = false;
} else{ $target.removeAttribute(name); }}Copy the code
- If the new node has attributes, then get the old node and all the attributes of the new node, traversal all the attributes of the old node, if the attribute is not in the new node, then the attribute is deleted. If the new node does not match the old node attribute/or is a new attribute
setProp
Add new attributes to real DOM nodes.
Update child nodes
Finally, similar to toRealDom, in updateDom we should also deal with all child nodes by recursively calling updateDom to compare vNodes of all child nodes one by one to see if they are updated. Once the VNode is updated, the real DOM also needs to be re-rendered:
// The root nodes are the same, but the children are different
if (
(oldNode.children && oldNode.children.length) ||
(newNode.children && newNode.children.length)
) {
for (let i = 0; i < oldNode.children.length || i < newNode.children.length; i++) { updateDom($currentDom, oldNode.children[i], newNode.children[i], i); }}Copy the code
Far from over
The above is the simplest Virtual DOM code implemented by the author, but there is a world of difference between this and the Virtual DOM algorithm used by the community. The author gives a simple example here:
<! -- old -->
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
Copy the code
<! -- new -->
<ul>
<li>5</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
Copy the code
For the updateDom function implemented in the above code, if the DOM structure before and after the update is as shown above, all five Li nodes will be re-rendered, which is obviously a waste of performance. However, SNabbDOM solves the above problems better by moving nodes. Due to the limited space of this paper and the community also has many articles on the analysis of the Virtual DOM algorithm, the author will not elaborate too much in this paper. Interested readers can go to their own research. Based on the examples in this paper, the author also realized the final version by referring to the SNabbDOM algorithm. Interested readers can check the final version of the example in this paper