With the rapid development of the front-end field, more and more front-end frameworks are emerging. React and Vue front-end frameworks have become essential skills for front-end developers.
I was often asked in interviews: Do you know anything about the virtual DOM? Just a quick word about diff, right? Have you studied the source code for the React/Vue framework and how dom-diff is implemented?
Now, as an interviewer myself, I often ask interviewees these questions. Although these questions are not useful for real project development, understanding the principles is very important for one’s technical growth!
Next, I will implement a virtual DOM step by step and explain the core logic and algorithms. Illustrated, so that the need for small partners can easily understand and summarize their understanding of the virtual DOM for later review.
What is the virtual DOM
The Virtual DOM is the Virtual DOM node. It simulates nodes in the DOM through JS objects and then renders them as real DOM nodes using a specific render method.
Why is manipulating DOM performance expensive
As you can see from the figure above, the actual DOM element is very large, because browser standards make the DOM very complex. When we do frequent DOM updates, there are performance issues. Virtual DOM uses a native JS object to describe a DOM node, so it costs much less than creating a DOM.
The real DOM is converted to the virtual DOM
The virtual DOM is a normal JavaScript object that contains attributes tag, props, and children.
<div id="app">
<p class="text">TXM to SFM</p>
</div>
Copy the code
The HTML element above is converted to the virtual DOM:
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'TXM to SFM']]}}Copy the code
Next, let’s take a closer look at how the HTML above is converted into the virtual DOM object with the JS tree structure below.
Initialize the project
Create a project
We use the React scaffolding to create a project that facilitates debugging, compiling, and opening effects.
Sudo NPM install -g create-react-app create-react-app dom-diffcdDom-diff // Compile NPM run startCopy the code
Virtual DOM
CreateElement core method
CreateElement a method that takes type, props, and children to create a DOM virtual tag element.
function createElement(type, props, children) {
return new Element(type, props, children);
}
Copy the code
To improve the high degree of reusability of the code, we put the core logical code that creates the virtual DOM Element into the Element class.
class Element {
constructor(type, props, children) {
this.type = type; this.props = props; this.children = children; }}Copy the code
Note: Mount these parameters to the object’s private properties so that they will also be present when new.
Render core method
The Render method takes a virtual node object argument that converts the virtual DOM to the real DOM.
function render(eleObj) {
letel = document.createElement(eleObj.type); // Create the elementfor(let key inEleobj.props) {// Methods for setting propertiessetAttr(el, key, eleobj.props [key])} eleobj.children. ForEach (child => { Create text node child = (child instanceof Element)? render(child) : document.createTextNode(child); el.appendChild(child); });return el;
}
Copy the code
Note: When converting a virtual DOM to a real DOM, there are several cases to consider when converting properties. Attributes such as value and style require special processing. For details on the processing logic, see the element Setting properties section below.
Element setting properties
The public method for assigning attributes to elements takes three arguments: node,key, and value, which represent the element to assign attributes to, the name of the attribute to, and the value of the attribute to.
function setAttr(node, key, value) {
switch(key) {
case 'value'// node is an input or textareaif(node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
node.value = value;
} else{// Common node.setAttribute(key, value); }break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break; }}Copy the code
Above, we only considered the attributes of the three cases. When we finished setting the attributes, we also need to judge the situation of the children attribute. For specific processing logic, please refer to the recursive Setting of sons section below.
Recursively set the son
Determines whether a child Element is an Element type, recursively, or creates a text node if it is not. Note: We only consider element types and text types.
Eleobj.children. ForEach (child => {// Check whether the child Element is of Element type, if not, create a text node child = (child instanceof Element)? render(child) : document.createTextNode(child); el.appendChild(child); });Copy the code
The real DOM is rendered to the browser
As we all know, the Render method converts the virtual DOM to the real DOM, but in order for the browser to see this effect, we need to add the real DOM to the browser. We write a method that takes el real DOM and target render target.
function renderDom(el, target) {
target.appendChild(el);
}
Copy the code
After the appeal step is complete, these methods can be exported for other use.
export {
createElement,
render,
Element,
renderDom
}
Copy the code
DOM Diff
Dom Diff returns a patch object through calculation at the JS level, that is, the patch object. The patch object is resolved through specific operations to complete the re-rendering of the page.
The Diff algorithm
Rule: Compare on the same level
There are many cases in the Diff algorithm. Next, we will discuss several common cases:
- {type:’ATTRS’, ATTRS: {class: ‘list-group’}}
- New DOM node does not exist {type: ‘REMOVE’, index: XXX}
- {type: ‘REPLACE’, newNode: newNode}
- {type: ‘TEXT’, TEXT: 1}
The core diff method of comparing two virtual DOM trees accepts two parameters oldTree old DOM tree and newTree new DOM tree, creates a patch according to the two virtual objects, describes the changed content, and uses the patch to update DOM. The core of this method is the Walk recursive tree, which puts the difference nodes after comparison into the patch package. For the core logic of the recursive tree, please refer to the walk Recursive Tree section below.
function diff(oldTree, newTree) {
let patches = {};
letindex = 0; Walk (oldTree, newTree, index, patches);return patches;
}
Copy the code
Note: When comparing differences between two trees, the default is to compare the first layer of the tree first.
Walk the recursive tree
The recursive tree method accepts four parameters, oldNode oldNode,newNode newNode,index comparison layer and patches, and returns common differences of various judgment situations and stores them in patch packages.
Case 1: A child node is deleted from the new node
currentPatch.push({type: REMOVE, index: index});
Copy the code
Case 2: Determine whether the two texts are the same
currentPatch.push({type: TEXT, text: newNode});
Copy the code
Note: currently, only text strings are judged; numbers are also judged.
When determining whether two texts are consistent, we first need to determine whether they are text types. For the sake of program extensibility, we encapsulate a public method to determine whether two texts are strings:
function isString(node) {
return Object.prototype.toString.call(node) === '[object String]';
}
Copy the code
Case 3: two nodes of the same element type, compare attributes
We need to encapsulate a diffAttr method when comparing whether the attribute is updated. For the core logic, see the diffAttr attribute comparison section below.
letattrs = diffAttr(oldNode.props, newNode.props); // Determine whether the result of the changed attribute has a valueif(object.keys (attrs).length > 0) {// Property changed currentPatch.push({type: ATTRS, attrs})
}
Copy the code
Note: after the first layer comparison, if there are sons, we need to traverse the recursive sons to find all the different patches in the number of two nodes. We need to encapsulate a diffChildren method, see the diffChildren Traversal son section below for the core logic.
diffChildren(oldNode.children, newNode.children, patches);
Copy the code
Case 4: Both are different and the node is replaced
currentPatch.push({type: REPLACE, newNode});
Copy the code
After determining the above conditions, you need to determine that the current element does have a patch, and then return the patches object assigned to the custom patch.
The diffAttr attribute is compared
This method accepts two parameters, oldAttrs old node attribute and newAttrs new node attribute, which are used to compare whether the attributes of the two nodes are the same and store the different ones in the patch object.
In attribute comparison, there are two cases to judge whether the attributes of the old and new nodes are different.
-
Determine the relationship between old properties and new properties
for(let key in oldAttrs) { if(oldAttrs[key] ! == newAttrs[key]) { patch[key] = newAttrs[key]; // Store the new property in the patch object, possibly undefined(if there is no property in the new property in the old property)}}Copy the code
-
The old node has no properties in the new node
for(let key inNewAttrs) {// The old node has no attributes in the new nodeif(!oldAttrs.hasOwnProperty(key)) { patch[key] = newAttrs[key]; } } Copy the code
DiffChildren Traversal son
The diffChildren method accepts oldChildren’s old son node, newChildren’s new son node, and patches patch object.
function diffChildren(oldChildren, newChildren, patches) {
oldChildren.forEach((child, idx) => {
walk(child, newChildren[idx], ++Index, patches);
});
}
Copy the code
Note: index increment is not traversal of IDX and index, but needs to define an index = 0 globally.
Patch Patch
When we obtain the patch through the Diff algorithm, we update the DOM through patch to update the page view.
The core method of patch is patch, which accepts node element node and patches. The function of patch is to patch elements and update views. The logic at the heart of this method is the Walk method. Read on for the walk patching each element section below.
functionpatch(node, patches) { console.log(node) allPatches = patches; // Patch an element walk(node); }Copy the code
Walk patches each element
This method takes a node element node as an argument, executes the patch again and again, and obtains the child nodes of the element for recursive traversal. If a patch exists for each layer, the doPatch method is executed. See the doPatch section below for details on the core logic of this method.
function walk(node) {
let currentPatch = allPatches[index++];
let childNodes = node.childNodes;
childNodes.forEach(child => walk(child));
if(currentPatch) {
doPatch(node, currentPatch); }}Copy the code
Note: allPatches and index variables need to be defined globally.
doPatch
The doPatch method accepts the node node and patches patch, and then iterates the patch to determine the patch type to perform different operations:
- When the patch
type
For the ATTR attribute, the attrs object is iterated to get the value. If the value existssetAttrSet the property value. If the value does not exist, delete the property.setAttrRead the core logic of the method belowSetAttr Sets propertiesSection. - When the patch
type
If it is TEXT, the TEXT of the patch is directly assigned to the corresponding nodetextContent
. - When the patch
type
When the REMOVE attribute is used, the parent is called directlyremoveChild
Delete the node. - When the patch
type
With the REPLACE attribute, you first need to determine whether the new node isElement
Element type, if so, called directlyrender
Method to re-render the new node; If not, passcreateTextNode
Create a text node. Finally, call the parent replaceChild method to replace the new node.
function doPatch(node, patches) {
patches.forEach(patch => {
switch(patch.type) {
case 'ATTRS':
for(let key in patch.attrs) {
let value = patch.attrs[key];
if(value) {
setAttr(node, key, value);
} else{ node.removeAttribute(key); }}break;
case 'TEXT':
node.textContext = patch.text;
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
case 'REPLACE':
let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode);
node.parentNode.replaceChild(newNode, node);
break;
default:
break; }})}Copy the code
SetAttr Sets properties
For details about the logic for the setAttr method to set properties, see the element Setting properties section above.
validation
Create the index.js file in the project root directory and verify the diff algorithm logic we wrote above by manually modifying the DOM structure.
import {createElement, render, renderDom} from './element';
import diff from './diff'
import patch from './patch'
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'},'a']),
createElement('li', {class: 'item'},'a']),
createElement('li', {class: 'item'},'b']]);let virtualDom2 = createElement('ul', {class: 'list-group'}, [
createElement('li', {class: 'item'},'1']),
createElement('li', {class: 'item'},'a']),
createElement('div', {class: 'item'},'3']]);letdom = render(virtualDom); RenderDom (DOM, window.root); renderDom(DOM, window.root); // console.log(dom);letpatches = diff(virtualDom, virtualDom2); // Update the view patch(DOM, patches);Copy the code
conclusion
From the beginning to the end, I believe many friends will feel that the whole process of DOM-Diff is very clear. The specific steps are as follows:
- Simulating the DOM with JS objects (virtual DOM)
- Render this virtual DOM into a real DOM and insert it into the render page.
- If an event occurs that modifies the virtual DOM, compare the difference between two virtual DOM trees to obtain the difference object (DIFF).
- Applying difference objects to real DOM trees (patches)
If this article is more helpful than a friend in need, please help to click a red heart plus wave of attention. star~
This article project warehouse address: https://github.com/tangmengcheng/dom-diff.gitCopy the code
If this article has helped you, give it a thumbs up ❤️❤️❤️
Welcome to join us to learn the front end and make progress together!