preface
Manipulating the Dom can be very performance intensive, as each element contains many attributes because browser standards make the Dom very complex. Virtual Dom uses a native JS object to describe a Dom node, namely VNode, so it is much cheaper than creating a real Dom element. React and Vue do just that. Let’s see how to implement a simple Virtual Dom. Complete code GitHub. If you like, I hope to point a little star
The core
- Using JavaScript object structure to represent the DOM tree structure; Then use this tree to build a real DOM tree
- When the state changes, a new object tree is rebuilt. Then compare the new tree with the old tree and note the differences between the two trees
- Applying the differences recorded in 2 to the actual DOM tree built in Step 1, the view is updated
Build vDOM
First we need to build the vDom, using JS objects to describe the real DOM tree. Once we have built the vDom, we need to render it on our page
// createElement.js
// give some default value.
export default (tagName, {attrs = {}, children = []} = {}) => {
return {
tagName,
attrs,
children
}
}
// main.js
import createElement from './vdom/createElement'
const createVApp = (count) => createElement('div', {
attrs: {
id: 'app',
dataCount: count
},
children: [
createElement('input'), // DOM redraw to render Input unfocused String(count), // Text node createElement('img', {
attrs: {
src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1555610261877&di=6619e67b4f45768a359a296c55ec1cc3&i mgtype=0&src=http%3A%2F%2Fimg.bimg.126.net%2Fphoto%2Fmr7DezX-Q4GLNBM_VPVaWA%3D%3D%2F333829322379622331.jpg'}})]})let count = 0;
let vApp = createVApp(count);
Copy the code
The following is the vDom constructed.
Then let’s look at the Render method, which converts our vDom to a real element.
// render.js
const renderElem = ({ tagName, attrs, children}) => {
// create root element
let $el = document.createElement(tagName);
// set attributeds
for (const [k, v] of Object.entries(attrs)) {
$el.setAttribute(k, v); } / /set children (Array)
for (const child of children) {
const $child = render(child);
$el.appendChild($child);
}
return $el;
}
const render = (vNode) => {
// if element node is text, and createTextNode
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
// otherwise return renderElem
return renderElem(vNode);
}
export default render
Copy the code
Then we go back to main.js
// Introduce render. Js module const$app= render(vApp); // Start building the actual DOMlet $rootEl = mount($app, document.getElementById('app')); / / create the mount. Jsexport default ($node.$target) => {
// use $node element replace $target element!
$target.replaceWith($node);
return $node;
}
Copy the code
And then finally you can see the effect. Isn’t he handsome? O (studying studying) O ha ha ~ ~ ~ ~
Now let’s do something fun. Back in main.js, we add the following code:
setInterval(() => {
count++;
$rootEl = mount(render(createVApp(count)), $rootEl); // $rootElThat's the whole real dom}, 1000)Copy the code
And then we go back to our page, see anything? You can try to type something into the input, and see any exceptions?
Looking at the source code, it turns out that every second we refresh the page. If we fill out a form and we break our fingers, we refresh the page. Guess what? Do you want to smash the computer? Don’t worry, diff algorithm can help us solve this headache problem!
diff
I’m not going to introduce the diff algorithm here, but you can find a lot of answers on the Internet. Go straight to the code!
// diff.js
import render from './render'
const zip = (xs, ys) => {
const zipped = [];
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
zipped.push([xs[i], ys[i]]);
}
return zipped;
};
const diffAttributes = (oldAttrs, newAttrs) => {
const patches = [];
// set new attributes
// oldAttrs = {dataCount: 0, id: 'app'}
// newAttrs = {dataCount: 1, id: 'app'}
// Object.entries(newAttrs) => [['dataCount', 1], ['id'.'app']]
for(const [k, v] of Object.entries(newAttrs)) {
patches.push($node= > {$node.setAttribute(k, v);
return $node;
})
}
// remove old attribute
for(const k in oldAttrs) {
if(! (kin newAttrs)) {
// $nodeIs the entire real DOM Tree patches. Push ($node= > {$node.removeAttribute(k);
return $node; }}})return $node= > {for (const patch of patches) {
patch($node);
}
}
}
const diffChildren = (oldVChildren, newVChildren) => {
const childPatches = [];
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {
childPatches.push(diff(oldVChild, newVChild));
}
const additionalPatches = [];
for (const additionalVChild of additionalPatches.slice(oldVChildren.length)) {
additionalPatches.push($node= > {$node.appendChild(render(additionalVChild));
return $node; })}return $parent= > {for (const [patch, child] of zip(childPatches, $parent.childNodes)) {
patch(child);
}
for (const patch of additionalPatches) {
patch($parent);
}
return $parent;
}
}
const diff = (vOldNode, vNewNode) => {
// remove all
if (vNewNode === 'undefined') {
return $node=> {// node.remove () to remove the object from the DOM tree to which it belongs.$node.remove();
return undefined;
};
}
// when element is textnode (like count)
if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {
if(vOldNode ! == vNewNode) {return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
} else {
return $node=> undefined; }}if(vOldNode.tagName ! == vNewNode.tagName) {return $node => {
const $newNode = render(vNewNode);
$node.replaceWith($newNode);
return $newNode;
};
}
const patchAttrs = diffAttributes(vOldNode.attrs, vNewNode.attrs);
const patchChildren = diffChildren(vOldNode.children, vNewNode.children);
return $node => {
patchAttrs($node);
patchChildren($node);
return $node;
};
};
export default diff;
// main.js
setInterval(() => { count++; // Every second, redraw the page, input out of focus (defect) //$rootEl = mount(render(createVApp(count)), $rootElConst vNewApp = createVApp(count); // New vDom const patch = diff(vApp, vNewApp); // Compare the differences$rootEl = patch($rootEl); vApp = vNewApp; // Every second is updated and saved for the next comparison. }, 1000).Copy the code
Nonsense less say, see the effect first (: ~~
SetInterval will count++ every time, and only the changed attributes and text will be updated on the page. This is the power of the diff algorithm.
Analysis of a wave
- diff
The diff function takes two arguments, vOldNode and vNewNode.
- Check whether vNewNode is undefined. What if the entire tree is deleted? Remove $node.remove()
- If you just change the name of the tag, then it’s ok to render directly and then replaceWith.
- If the new and old nodes are of type ‘string’, then you have to determine whether the new and old nodes are equal!
- All of the resulting differences are thrown into patches, which are a function and receive the parameter $rootEl
- diffAttributes
Compare attributes to do, is to get the new vDom attributes, and then traverse the old vDom attributes, determine whether the old vDom attributes exist in the new vDom. The key point is that I’m going to describe it
- The object.entries () method returns an array of key-value pairs for the given Object’s own enumerable properties, arranged in the same order as for… The in loop returns the same order as it iterates through the object (the difference is that the for-In loop also enumerates properties in the prototype chain)
- Walk through the oldAttrs for for and get all the keys in the old vDom
- The in operator is used to determine whether the key in 2 exists in newAttrs.
- Finally, a function is returned that receives $rootEl and iterates through patches with the attributes compared. Each term is a function.
- diffChildren
And finally, there’s children.
- Accepts two arguments, oldVChildren and newVChildren
- The main thing here is the ZIP function. Get the child of the old and new nodes, and store the old and new nodes of each node into an array, as shown in the figure below:
- It then iterates through the zipped array. Continue diff and save the diff result
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {
childPatches.push(diff(oldVChild, newVChild));
}
Copy the code
conclusion
Virtual DOM is the core part of the Diff algorithm, here is still more complex, need more practice and repeated thinking, well, today’s introduction to this, if you like it, you can praise oh!