Skip to main content

Deep into the Virtual DOM

· 6 min read

VDOM is the abbreviation of Virtual DOM(Virtual DOM). It refers to the DOM structure simulated by JS, and the comparison of DOM changes is done on the JS layer. In other words, A VDOM is a JS object.

for example simple dom structure is:

<ul id="list">
<li class="item">Item1</li>
<li class="item">Item22</li>
</ul>

real DOM to a virtual DOM is just that, a JS object.

{
tag: "ul",
attrs: {
id: "list",
},
children: [
{
tag: "li",
attrs: { className: "item" },
children: ["Item1"],
},
{
tag: "li",
attrs: { className: "item" },
children: ["Item2"],
},
],
};

Why need virtual DOM​

Suppose you now have a scenario that implements the following requirements:

[
{
name: "aaa",
age: "20",
address: "American",
},
{
name: "bbb",
age: "21",
address: "American",
},
{
name: "ccc",
age: "22",
address: "Chinese",
},
];

Present this data as a table, and modify any information you want, and the table will change as well. JQuery implementation is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>

<body>
<div id="container"></div>
<button id="btn-change">change table data</button>
<script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script>
const data = [
{
name: "aaa",
age: "20",
address: "American",
},
{
name: "bbb",
age: "21",
address: "American",
},
{
name: "ccc",
age: "22",
address: "Chinese",
},
];
// render table
function render(data) {
const $container = $("#container");
$container.html("");
const $table = $("<table>");
// rerender
$table.append($("<tr><td>name</td><td>age</td><td>address</td></tr>"));
data.forEach((item) => {
// render row
$table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`));
});
$container.append($table);
}

$("#btn-change").click(function () {
data[1].age = 30;
data[2].address = "Japan";
render(data);
});
</script>
</body>
</html>

When click the button, there will be a corresponding view changes, but you inspect the html elements, after each change, table tags have to recreate, every column that is under the table, whether the data is the same, it will have to apply colours to a drawing, it's not an ideal situation, when one of the column data as well as the original, We would prefer that this column not be re-rendered, because DOM redrawing is quite a drain on browser performance.

Therefore, we adopt the method of JS object simulation, and put the DOM comparison operation in the JS layer to reduce unnecessary redrawing of the browser and improve efficiency.

Of course, some people say that the virtual DOM is no faster than the real DOM, and there is truth to that. When each item in the table above changes, it is obvious that the real DOM is faster because the virtual DOM also has the comparison process of the diff algorithm in JS. Therefore, the above performance benefits only apply when rendering a large amount of data and changing only a small fraction of the data.

What makes the virtual DOM even better is:

  1. It opens the door to functional UI programming, the way UI = F (data)

  2. Render JS objects to environments outside the BROWSER DOM, which supports cross-platform development such as ReactNative.

VDOM implemented using Snabbdom​

Snabbdom is a simple library to implement VDOM functions. Compared with VUE and React, it is easier for us to learn VDOM. There are two core apis in VDOM, one is h function, the other is patch function, the former is used to generate VDOM objects, the latter function is to do virtual DOM comparison and mount VDOM to the real DOM.

A brief introduction to the use of these two functions:

  • h('tag name', {attribute}, [child element])
  • h('tag name', {attribute}, [text])
  • patch(container, vnode) container is the DOM element of a container
  • patch(vnode, newVnode)

Now let's rewrite the example using snabbdom:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">改变</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
let snabbdom = window.snabbdom;

// 定义patch
let patch = snabbdom.init([snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners]);

//定义h
let h = snabbdom.h;

const data = [
{
name: "aaa",
age: "20",
address: "American",
},
{
name: "bbb",
age: "21",
address: "American",
},
{
name: "ccc",
age: "22",
address: "Chinese",
},
];

let container = document.getElementById("container");
let vnode;
const render = (data) => {
let newVnode = h(
"table",
{},
data.map((item) => {
let tds = [];
for (let i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h("td", {}, item[i] + ""));
}
}
return h("tr", {}, tds);
})
);

if (vnode) {
patch(vnode, newVnode);
} else {
patch(container, newVnode);
}
vnode = newVnode;
};

render(data);

let btnChnage = document.getElementById("btn-change");
btnChnage.addEventListener("click", function () {
data[1].age = 30;
data[2].address = "Japan";
// re-render
render(data);
});
</script>
</body>
</html>

When you click the button, inspect element shows that only the element corresponding to the changed data is updated, maximizing reuse of DOM nodes

What is the diff algorithm?​

The diff algorithm is an algorithm used to find the difference between two paragraphs of text.

As a front-end, we often hear the word diff algorithm, in fact, is not the front-end original algorithm, in fact, this algorithm has long been reflected in the linux diff command, and everyone commonly used git diff is also using the diff algorithm.

Why does VDOM use diff algorithm​

DOM manipulation is very expensive, so we need to minimize DOM manipulation. In this case, it is necessary to find out which nodes the DOM must update, and not to update the others. In this process, it is necessary to apply the DIff algorithm.

Simple implementation of diff algorithm in VDOM​

The following code is intended to help you understand the principles and flow of the DIff algorithm and is not intended for use in production environments.

convert VDOM to real DOM:

const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if (!tag) {
return null;
}
let elem = document.createElement(tag);
let attrName;
for (attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
// create child nodes if has children
children.forEach((childVnode) => {
elem.appendChild(createElement(childVnode));
});
return elem;
};

Update with simple diff algorithm:​

function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];

children.forEach((childVnode, index) => {
let newChildVNode = newChildren[index];
if (childVnode.tag === newChildVNode.tag) {
// same element, just update
updateChildren(childVnode, newChildVNode);
} else {
// different element, replace
replaceNode(childVnode, newChildVNode);
}
});
}