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:

  1. {type:’ATTRS’, ATTRS: {class: ‘list-group’}}
  2. New DOM node does not exist {type: ‘REMOVE’, index: XXX}
  3. {type: ‘REPLACE’, newNode: newNode}
  4. {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.

  1. 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
  2. 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:

  1. When the patchtypeFor 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.
  2. When the patchtypeIf it is TEXT, the TEXT of the patch is directly assigned to the corresponding nodetextContent.
  3. When the patchtypeWhen the REMOVE attribute is used, the parent is called directlyremoveChildDelete the node.
  4. When the patchtypeWith the REPLACE attribute, you first need to determine whether the new node isElementElement type, if so, called directlyrenderMethod to re-render the new node; If not, passcreateTextNodeCreate 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:

  1. Simulating the DOM with JS objects (virtual DOM)
  2. Render this virtual DOM into a real DOM and insert it into the render page.
  3. If an event occurs that modifies the virtual DOM, compare the difference between two virtual DOM trees to obtain the difference object (DIFF).
  4. 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!