Vue3 - Virtual DOM&diff source code

Virtual DOM

node

Virtual DOM is a JS abstract representation of DOM. They are JS objects that can describe the structure and relationship of DOM. Various state changes of the application will be reflected on the virtual Dom and finally mapped to the real dom.

Before rendering the virtual DOM, we need to make some preparations. By observing the real Dom and components, we can know:

  • Virtual DOM needs to have its own types, such as HTML tags, pure text, components (components are divided into class components and function components), etc. because we are implementing a simple virtual DOM, we only implement plain text and HTML tags

  • We know that the real DOM is similar to a tree structure, so we need to know the type of DOM sub elements, whether they are single sub elements, multiple sub elements or empty (plain text or empty)

  • At the same time, the virtual DOM is a description of the real dom. For a single element, we can simply summarize it as label and attribute (attributes include style, id, class, click event, etc.)

So we first need to define some constants to distinguish the above

//Type of virtual DOM
    const vnodeType={
        HTML:'HTML',//html tag
        TEXT:'TEXT',//pure text
        COMPONENT:'COMPONENT',//function component
        CLASS_COMPONENT:'CLASS_COMPONENT',//class component
    }
// Type of child element
    const childType = {
        EMPTY:'EMPTY',//The child element is empty (this simply refers to the case of plain text)
        SINGLE:'SINGLE',//Single child element
        MULTIPLE:'MULTIPLE'//There are multiple child elements
    }

Because we often need to use the real DOM corresponding to vnode when updating in the future, we define a field to store the dom. In subsequent update operations, we need to use the key attribute to perform diff algorithm when updating old and new DOMS

/**
 * New virtual DOM
 * @param {string} tag Because it implements a simple virtual DOM, this tag is a simple element tag name
 * @param {obj} data attribute
 * @param {obj} children Child element
 */
function createElement(tag, data, children = null) {
    //New virtual DOM
   ..........................................
    //Return virtual DOM: vnode
    return {
        flag,//The type used to mark the current vnode
        tag,//Tags: div, text (empty), component (a function, not involved temporarily)
        data,//data
        children,//Child element
        childrenFlag,//Types of children
        el:null//Store the real dom, which defaults to null at the beginning
    }
}
/**
 * Handle vnode where children become text type
 * @param {*} text children element
 */
function createTextVnode(text) {
    return {
        flag: vnodeType.TEXT,
        tag: null,
        key:data&&data.key,
        children: text,
        childrenFlag: childType.EMPTY,
        el: null
    }
}

Render

When there are nodes, start rendering nodes

 function render(){
        //First, we need to distinguish between first rendering and second rendering
        mount(vnode,container)
    }

    /**
    * Render virtual DOM for the first time, Mount
    * @param {obj} vnode Virtual DOM
    * @param {*} container Render container
    * @param {} flagNode It is used to determine whether to insert when
    */
    function mount(vnode,container,flagNode){
        //First, render according to the flag of vnode
        let {flag} = vnode
        //Here, the mounting of the execution mode is determined according to the type of vnode
        if(flag === vnodeType.HTML){
            //If html tag
            mountElement(vnode,container,flagNode)
        }else if(flag ===vnodeType.TEXT){
            //If text is plain text
            mountText(vnode,container)
        }
    }

    /**
    * Mount virtual DOM of html type
    * @param {obj} vnode Virtual DOM
    * @param {*} container container
    * @param {} flagNode It is used to determine whether to insert when
    */
    function mountElement(vnode,container,flagNode){
        //vnode of html type creates dom based on tag name
        let dom = document.createElement(vnode.tag)
        //When you first mount, mount the real dom corresponding to the current vnode to the current vnode for later use when you mount child elements
        vnode.el =dom
        let {data,children,childrenFlag} = vnode

        //Mount properties
        if (data) {
            for (let key in data) {
                //Mount data
                patchData(vnode.el, key, null, data[key])
            }
        }
        //Start mounting child elements
        if(childrenFlag !==childType.EMPTY){
            //If the child element is not empty
            if(childrenFlag===childType.SINGLE){
                //Mount child element
                mount(children,vnode.el)
            }else if(childrenFlag===childType.MULTIPLE){
                for(let i=0;i<children.length;i++){
                    mount(children[i],vnode.el)
                }
            }
        }
        //Mount dom
        flagNode?container.insertBefore(el, flagNode):container.appendChild(dom)
    }
    /**
    * Mount virtual DOM of plain text type
    * @param {odj} vnode Virtual DOM
    * @param {*} container container
    */
    function mountText(vnode, container) {
        //For vnode of plain text type, the child element is text, so it is executed directly
        let dom = document.createTextNode(vnode.children)
        vnode.el = dom
        container.appendChild(vnode.el)
    }

Mount properties

When rendering the virtual DOM, we also need a method to render the data attribute, that is, the attachment of the attribute. When attaching attributes, we need to distinguish them and deal with different attributes differently.

/**
    * Mount properties
    * @param {*} dom Node real dom
    * @param {string} key data Corresponding key
    * @param {obj} preData data Old value
    * @param {obj} newData data New value
    */
    function patchData(dom, key, preData, newData) {
        //Render in different ways according to different types of attributes
        switch (key) {
            case 'style':
                for (let k in newData) {
                    //Mount the corresponding properties
                    dom.style[k] = newData[k]
                }
                //Some attributes need to be deleted during patch
                break;
            case 'class':
                dom.className = newData
                break;
            default:
                if (key[0] === '@') {
                    //The presence of the @ symbol is considered a click event
                    if (newData) {
                        dom.addEventListener(key.split(1), newData)
                    }
                } else {
                    //Otherwise, we'll deal with it in a rough way here
                    dom.setAttribute(key, newData)
                }
                break;
        }
    }

Virtual DOM update

The previous code implements the initial mounting of the virtual dom. But when we make changes to the virtual DOM, what we need is an update operation. At the same time, the update operation is also a complex part of the render process.

Change rendering method

 function render(vnode, container) {
        //First, we need to distinguish between first rendering and second rendering
        if(container.vnode){
            patch(container.vnode,vnode,container)
        }else{
            mount(vnode, container)
        }
        //After mounting, mount the vnode into the container to judge whether it is the first rendering or the subsequent update rendering
        container.vnode = vnode
    }

Update method: patch

The main function of this function is to distinguish the update operation according to the flag attribute in new and old vnode s. The replacement operation and update text operation are relatively simple
Both initialization and update are completed by patch

 function patch(prev,next,container){
        let nextFlag = next.flag
        let prevFlag = prev.flag
        //Different processing is carried out according to the types of new and old virtual DOM S
        if(nextFlag!==prevFlag){
            //If the flag types are different, we directly perform the replacement operation
            replaceVnode(prev,next,container);
        }else if(nextFlag==vnodeType.HTML){
            //Update element
            patchElement(prev,next,container)
        }else if(nextFlag==vnodeType.TEXT){
            //Update text
            patchText(prev,next)
        }
    }

    /**
    * Update Text
    * @param {ovj} prev Old vnode
    * @param {*} next New vnode
    */
    function patchText(prev,next){
        let el = (next.el = prev.el)
        if(next.children!==prev.children){
            //Directly change text in dom
            el.nodeValue = next.children
        }
    }
    /**
    * Replace operation to update virtual DOM
    * @param {obj} prev Old vnode
    * @param {obj} next New vnode
    * @param {*} container container
    */
    function replaceVnode(prev,next,container){
        //Replace directly
        container.removeChild(prev.el)
        mount(next,container)
    }
const patchVNode = (oldVNode, newVNode) => {
    // The element labels are the same, and patch
    if (oldVNode.tag === newVNode.tag) {
        // If the element types are the same, the old elements must be reused
        let el = newVNode.el = oldVNode.el
        // The child node of the new node is the text node
        if (newVNode.text) {
            // Remove child nodes of old nodes
            if (oldVNode.children) {
                oldVNode.children.forEach((item) => {
                    el.removeChild(item.el)
                })
            }
            // Update the text if the text content is different
            if (oldVNode.text !== newVNode.text) {
                el.textContent = newVNode.text
            }
        } else {
            // ...
        }
    } else { // Replace oldNode with newNode differently
        // ...
    }
}

Update the new node. Here we use diff to compare the old and new nodes

1. Get the types of child nodes and child nodes. There are three types of child nodes in the new node n2 and the old node n1: text node, array node and empty node
2. The old node is an empty text node, and then the old node is an empty text node
3. If the new node is a text node and the old node is a text node, compare whether the text of the old and new nodes is consistent. If not, replace the text content
4. If the old node is an array node and the new node is also an array node, enter patchKeyedChildren, which is the process of diff
5. If the old node is an array node and the new node is an empty node, go to delete the old node
6. In this way, the remaining situation is that the previous node is either a text node or empty, while the new node is either an array or empty
7. Therefore, if the previous node is a text node, delete the text content of the old node
8. If the new node is an array, add the new node to the dom tree
9. Finally, the new node is left as an empty node without any operation (the old node has been deleted at point 7)

reference resources

Explain the virtual DOM and Diff algorithm in simple terms, and the differences between Vue2 and Vue3

Keywords: Javascript Front-end html

Added by [ArcanE] on Thu, 24 Feb 2022 10:39:48 +0200