Virtual DOM works by using JS objects to simulate the nodes of the DOM. At the early stage of building React, Facebook introduced virtual DOM in consideration of factors such as improving code abstraction ability, avoiding artificial DOM operation and reducing the overall risk of code.
How it works?
The process likes that use createElement to build virtual dom, and it return a plain object that describes its tag type, props properties, children, and so on. These Plain objects form a virtual DOM tree through a tree structure.
render function converts virtual dom into real dom, and mount real dom, like document.getElementById("root").appendChild(realDom)
.
When the status changes, There's diff functions to compare the difference of the virtual DOM tree before and after the change. Let's have a look below two graphs. There's four different places.
The diff function returns an object, includes all different nodes information, we can call it patches.
doPatch function bases on patches objects, will patch one by one on real dom.
Code snap examples
How createElement looks like?
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
function createElement(type, props, children) {
return new Element(type, props, children);
}
More definitions about React.createElement
How virtual dom looks like?
const oldVDom = createElement(
"ul",
{
class: "ul",
style: "width: 400px; height: 400px; background-color: pink;",
},
[
createElement(
"li",
{
class: "li-wrap",
"data-index": 0,
},
[
createElement(
"p",
{
class: "text",
},
["The first tag"]
),
]
),
createElement(
"li",
{
class: "li-wrap",
"data-index": 1,
},
[
createElement(
"p",
{
class: "text",
},
[
createElement(
"span",
{
class: "span",
},
["The second li tag, will be removed later on."]
),
]
),
]
),
createElement(
"li",
{
class: "li-wrap",
"data-index": 2,
},
["The third tag"]
),
]
);
How real dom looks like?
How render function looks like?
function render(vDom) {
// destructure virtual dom
const { type, props, children } = vDom;
// transform type
const el = document.createElement(type);
// transform all props
for (let key in props) {
// transform different attributes
setAttrs(el, key, props[key]);
}
children.map((c) => {
// recursive, if child is Element, then call render function again
// break condition is if child is not Element
c = c instanceof Element ? render(c) : document.createTextNode(c);
el.appendChild(c);
});
return el;
}
How to mount real dom?
function renderDom(rDom, rootEl) {
rootEl.appendChild(rDom);
}
renderDom(rDom, document.getElementById("root"));
How patches look like?
const patches = {
0: [
{
type: "ATTR",
attrs: {
class: "ul-wrap",
},
},
],
3: [
{
type: "TEXT",
text: "Update the first tag",
},
],
6: [
{
type: "REMOVE",
index: 6,
},
],
7: [
{
type: "REPLACE",
newNode: newNode,
},
],
};
How domDiff look like?
let patches = {};
let vnIndex = 0;
function domDiff(oldVDom, newVDom) {
let index = 0;
vNodeWalk(oldVDom, newVDom, index);
return patches;
}
function vNodeWalk(oldNode, newNode, index) {
let vnPatch = [];
/**
* node can be
* Element {type:'', props:{}, children: []}
* text string
* null
*/
if (!newNode) {
// item was removed
vnPatch.push({
type: REMOVE,
index,
});
} else if (typeof oldNode === "string" && typeof oldNode === "string") {
// text change
if (oldNode != newNode) {
vnPatch.push({
type: TEXT,
text: newNode,
});
}
} else if (oldNode.type === newNode.type) {
const attrPatch = attrsWalk(oldNode.props, newNode.props);
if (Object.keys(attrPatch).length > 0) {
vnPatch.push({
type: ATTR,
attrs: attrPatch,
});
}
childrenWalk(oldNode.children, newNode.children);
} else {
vnPatch.push({
type: REPLACE,
newNode,
});
}
if (vnPatch.length > 0) {
patches[index] = vnPatch;
}
}
function childrenWalk(oldChildren, newChildren) {
oldChildren.map((child, index) => {
vNodeWalk(child, newChildren[index], ++vnIndex);
});
}
How doPatch looks like?
let finalPatches = {};
let rnIndex = 0;
function doPatch(rDom, patches) {
finalPatches = patches;
rNodeWalk(rDom);
}
function rNodeWalk(rNode) {
const rnPatch = finalPatches[rnIndex++];
const childNodes = rNode.childNodes;
[...childNodes].map((child) => {
rNodeWalk(child);
});
if (rnPatch) {
patchAction(rNode, rnPatch);
}
}
function patchAction(rNode, rnPatch) {
rnPatch.map((patch) => {
switch (patch.type) {
case ATTR:
for (let key in patch.attrs) {
const value = patch.attrs[key];
if (value) {
setAttrs(rNode, key, value);
} else {
rNode.removeAttribute(key);
}
}
break;
case TEXT:
rNode.textContent = patch.text;
break;
case REPLACE:
const newNode =
patch.newNode instanceof Element
? render(patch.newNode)
: document.createTextNode(patch.newNode);
rNode.parentNode.replaceChild(newNode, rNode);
break;
case REMOVE:
rNode.parentNode.removeChild(rNode);
break;
default:
break;
}
});
}
Top comments (0)