Source code analysis of Vue view update patch process

Keywords: Javascript Vue Attribute less JQuery

In this article Deep source learning Vue responsive principle This article explains how Vue informs the data that depends on it to update when the data changes. The point of this article is: the view knows the change of the dependent data, but how to update the view.

Vnode Tree

There is a DOM tree corresponding to it in real HTML, and a similar Vnode Tree corresponding to it in Vue.

Abstract DOM tree

In the era of jquery, to implement a function is often to operate DOM directly to change the view. But we know that direct manipulation of DOM often affects redrawing and rearrangement, which are the two elements that most affect performance.
After entering the era of virtual DOM, the real DOM tree is abstracted into an abstract tree composed of js objects. Virtual DOM is the abstraction of real dom. It uses attributes to describe the characteristics of real dom. When the virtual DOM changes, modify the view. In Vue, it is the concept of Vnode Tree

VNode

When modifying a piece of data, js will replace the whole DOM Tree, which is quite performance consuming. Therefore, the concept of Vnode is introduced into Vue: Vnode is the simulation of real DOM nodes, which can add nodes, delete nodes and modify nodes for Vnode tree. These processes only need to operate Vnode tree, and do not need to operate the real DOM, which greatly improves the performance. After modification, use diff algorithm to calculate the modified minimum units, and update the views of these small units.

// core/vdom/vnode.js
class Vnode {
    constructor(tag, data, children, text, elm, context, componentOptions) {
        // ...
    }
}

Generate vnode

There are two ways to generate a vnode:

  1. Create a vnode of a non component node

    • tag does not exist, create empty node, comment, text node
    • Use vnode of element type listed inside vue
    • There are no vnode s listed for creating element types

Take < p > 123 < / P > as an example, two vnode s will be generated:

-Node with 'tag' as' p ', but no' text 'value
 -The other is a node without a 'tag' type but with a 'text' value
  1. Create VNode of component node

The Vnode generated by the component node does not correspond to the node of DOM Tree one by one, but only exists in VNode Tree

// core/vdom/create-component
function createComponent() {
    // ...
    const vnode = new VNode(
        `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
        data, undefined, undefined, undefined, context,
        { Ctor, propsData, listeners, tag, children }
    )
}
Create a component occupation 'vnode' here, and there will be no real 'DOM' node corresponding to it  

The establishment of component vnode is explained with the following examples:

<!--parent.vue-->
<div classs="parent">
    <child></child>
</div>
<!--child.vue-->
<template>
    <div class="child"></div>
</template>

The label child does not exist in the real rendered DOM Tree. child.vue is a sub component. In Vue, a occupied vnode will be created for this component. This vnode will not correspond to the DOM node one by one in the final DOM Tree, that is, it will only appear in the vnode Tree.

/* core/vdom/create-component.js */
export function createComponent () {
    // ...
     const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }
    )
}

The final Vnode Tree generated is as follows:

vue-component-${cid}-parent
    vue-component-${cid}-child
        div.child

The final DOM structure generated is:

<div class="parent">
    <div class="child"></div>
</div>

When printing itself in two component files, you can see the relationship between them
chlid instance object

parent instance object

You can see the following relationships:

  1. Parent vnode points to child vnode through children
  2. Child vnode points to parent vnode through $parent
  3. $vnode with occupation vnode as object
  4. The rendered vnode is the object's vnode

patch

As mentioned in the previous article, when creating a Vue instance, the following code will be executed:

updateComponent = () => {
    const vnode = vm._render();
    vm._update(vnode)
}
vm._watcher = new Watcher(vm, updateComponent, noop)

When the data changes, the callback function updateComponent will be triggered to update the template data. updateComponent is actually the encapsulation of "patch". The essence of patch is to compare the old and new vnode s, create or update DOM node / component instances. If it is the first time, then create DOM or component instances.

// core/vdom/patch.js
function createPatchFunction(backend) {
    const { modules, nodeOps } = backend;
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
          if (isDef(modules[j][hooks[i]])) {
            cbs[hooks[i]].push(modules[j][hooks[i]])
          }
        }
    }
    
    return function patch(oldVnode, vnode) {
        if (isUndef(oldVnode)) {
            let isInitialPatch = true
            createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
            } else {
                if (isRealElement) {
                    oldVnode = emptyNodeAt(oldVnode)
                }
                const oldElm = oldVnode.elm
                const parentElm = nodeOps.parentNode(oldElm)
                createElm(
                    vnode,
                    insertedVnodeQueue,
                    oldElm._leaveC ? null : parentELm,
                    nodeOps.nextSibling(oldElm)
                )
                
                if (isDef(vnode.parent)) {
                    let ancestor = vnode.parent;
                    while(ancestor) {
                        ancestor.elm = vnode.elm;
                        ancestor = ancestor.parent
                    }
                    if (isPatchable(vnode)) {
                        for (let i = 0; i < cbs.create.length; ++i) {
                            cbs.create[i](emptyNode, vnode.parent)
                        }
                    }
                }
                if (isDef(parentElm)) {
                    removeVnodes(parentElm, [oldVnode], 0, 0)
                } else if (isDef(oldVnode.tag)) {
                    invokeDestroyHook(oldVnode)
                }
            }
        }
        
        invokeInsertHook(vnode, insertedVnodeQueue)
        return vode.elm
    }
}
  • If this is the first patch, a new node is created
  • Old node exists

    • The old node is not a real DOM and is the same as the new node

      • Calling patchVnode to modify an existing node
    • New and old nodes are different

      • If the old node is a real DOM, create a real DOM
      • Create element / component instance for new Vnode, if parentElm exists, insert it on parent element
      • If the component root node is replaced, traverse to update the parent node element. Then remove the old node
  • Call the insert hook

    • This is the first patch and vnode.parent exists. Set vnode.parent.data.pendingInsert = queue
    • If the above conditions are not met, the insert hook is called for each vnode
  • Return to vnode.elm

nodeOpsFor various platformsDOMOperation,modulesRepresents various modules that providecreateandupdateHook, which is used to create the corresponding module after completion and update;Some modules also provideactivate,remove,destoryWait for hook. After treatmentcbsThe final structure of:

cbs = {
    create: [
        attrs.create,
        events.create
        // ...
    ]
}

Finally, the function returns the patch method.

createElm

The purpose of createElm is to create vnode.elm of VNode node. The creation process of vnode.elm is different for different types of VNode. For component occupation VNode, createComponent will be called to create component instance of component occupation VNode; for non component occupation VNode, corresponding DOM node will be created.
There are now three nodes:

  • VNode of element type:

    • Create the DOM element node corresponding to vnode vnode.elm
    • Set the scope of vnode
    • Call createChildren to create the DOM node of the child vnode
    • Execute the create hook function
    • Insert a DOM element into the parent element
  • Notes and article nodes

    • Create a comment / text node, vnode.elm, and insert it into the parent element
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    // Create a component node
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
        return
    }
    const data = vnode.data;
    const childre = vnode.children;
    const tag = vnode.tag;
    // ...

    if (isDef(tag)) {
        vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            : nodeOps.createElement(tag, vnode)
        setScope(vnode)
        if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        createChildren(vnode, children, insertedVnodeQueue)  
    } else if (isTrue(vnode.isComment)) {
        vnode.elm = nodeOps.createComment(vnode.text);
    } else {
        vnode.elm = nodeOps.createTextNode(vnode.te)
    }
    insert(parentElm, vnode.elm, refElm)
}

The main function of createComponent is to create a component instance of component occupying Vnode, initialize the component, and reactivate the component. Use the insert method to manipulate the DOM in the reactivated component. createChildren is used to create a child node. If the child node is an array, the createElm method is traversed and executed. If the text attribute of the child node has data, nodeOps.appendChild() is used to insert the text content in the real dom. Insert inserts elements into the real dom.

// core/vdom/patch.js
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    // ...
    let i = vnode.data.hook.init
    i(vnode, false, parentElm, refElm)
    if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        return true;
    }
}
  • Execute init hook to generate component instance
  • Call initComponent to initialize the component

    • Merge the existing vnode queues
    • Get the DOM root element node of the component instance and assign it to vnode.elm
    • If vnode is patch able

      • Call the create function to set the scope
    • If not patch

      • Register the ref of the component, and add the component occupation vnode to insertedVnodeQueue
  • Insert vnode.elm into DOM Tree

In the process of component creation, createComponent in core / vdom / create component will be called. This function will create a component vnode, and then create and declare each declaration cycle function on vnode. init is one of the cycles. It will create the componentInstance property for vnode. Here, componentInstance represents an instance that inherits Vue. During the process of new vnodeComponentOptions.Ctor(options), a Vue instance will be re created, and each life cycle will be re executed, such as created -- > mounted.

init (vnode) {
    // Create a subcomponent instance
    const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance)
    chid.$mount(undefined)
}
function createComponentInstanceForVnode(vn) {
    // ... definition of options
    return new vnodeComponentOptions.Ctor(options)
}

In this way, child represents a Vue instance. During the creation of the instance, various initialization operations will be performed, such as calling each life cycle. Then calling $mount will actually call the mountComponent function.

// core/instance/lifecycle
function mountComponent(vm, el) {
    // ...
    updateComponent = () => {
        vm._update(vm._render())
    }
    vm._watcher = new Watcher(vm, updateComponent, noop)
}

VM. Render will be executed here

// core/instance/render.js
Vue.propotype._render = function () {
    // ...
    vnode = render.call(vm._renderProxy, vm.$createElement)
    return vnode
}

When you can see it, you call the ﹣ render function, and finally a vnode is generated. Then calling vm._update and calling vm.__patch__ to generate the DOM Tree of the component, but not inserting DOM Tree to the parent element, if there are subcomponents in the subcomponent, it will create an instance of the descendant component and create DOM Tree of the descendant component. When insert(parentElm, vnode.elm, refElm) is called, the current DOM tree will be inserted into the parent element.
When returning to the patch function, when it is not the first rendering, another logic will be executed. Then, whether the oldVnode is the real DOM, if not, and whether the new and old vnodes are different, patchVnode will be executed.

// core/vdom/patch.js
function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.tag === b.tag && 
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType
    )
}

sameVnode is used to determine whether two vnodes are the same node.

patchVnode

If it conforms to sameVnode, it will not render vnode and recreate the DOM node, but repair the original DOM node and reuse the original DOM node as much as possible.

  • If two nodes are the same, return directly
  • Handling static nodes
  • vnode is patch able

    • Calling the prepatch hook of component occupation vnode
    • update hook exists, call update hook
  • Text text does not exist for vnode

    • New and old nodes all have children child nodes, and children are different, then call updateChildren to update children recursively (the content of this function will be explained in diff)
    • Only the new node has children: first empty the text content, and then add children for the current node
    • Only old nodes have children: remove all children
    • When there is no child node, the text of the node is removed directly
  • New and old node text is different: replace node text
  • Call the postpatch hook of vnode
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) return
    //Handler for static node
    const data = vnode.data;
    i = data.hook.prepatch
    i(oldVnode, vnode);
    if (isPatchable(vnode)) {
        for(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        i = data.hook.update
        i(oldVnode, vnode)
    }
    const oldCh = oldVnode.children;
    const ch = vnode.children;
    if (isUndef(vnode.text)) {
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
        nodeOps.setTextContent(elm, vnode.text)
    }
    i = data.hook.postpatch
    i(oldVnode, vnode)
}

diff algorithm

It is mentioned in patchVnode that if new and old nodes have children, but they are different, updateChildren will be called. This function uses diff algorithm to reuse previous DOM nodes as much as possible.

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, elmToMove, refElm 
    
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
            if (isUndef(idxInOld)) {
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                newStartVnode = newCh[++newStartIdx]
            } else {
                elmToMove = oldCh[idxInOld]
                if (sameVnode(elmToMove, newStartVnode)) {
                    patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
                    oldCh[idxInOld] = undefined
                    canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                } else {
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

Well, I didn't get the picture. I borrowed the picture from the Internet


oldStartIdx, newStartIdx, oldEndIdx and newEndIdx are the indexes on both sides of the new and old vnodes respectively. Meanwhile, oldStartVnode, newStartVnode, oldEndVnode and new EndVnode point to the corresponding vnode of these indexes respectively. The whole traversal needs to be done when oldStartIdx is less than oldEndIdx and newStartIdx is less than newEndIdx

  1. When oldStartVnode does not exist, oldStartVnode moves to the right, oldStartIdx plus 1
  2. When oldEndVnode does not exist, oldEndVnode moves to the right, oldEndIdx minus 1
  3. oldStartVnode and newStartVnode are similar. oldStartVnode and newStartVnode move to the right, and oldStartIdx and newStartIdx increase by 1

  1. oldEndVnode and newEndVnode are similar. Both oldEndVnode and newEndVnode move to the left, and oldEndIdx and newEndIdx are reduced by 1

  1. If oldStartVnode is similar to newEndVnode, move oldStartVnode.elm to the back of oldEndVnode.elm. Then oldStartIdx moves one bit backward and newEndIdx moves one bit forward

  1. When oldEndVnode and newStartVnode are similar, insert oldEndVnode.elm in front of oldStartVnode.elm. Similarly, oldEndIdx moves one bit forward and newStartIdx moves one bit backward.

  1. When none of the above conditions are met

Generate a hash table corresponding to the key and the old vnode

function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}

The final generated object is the object with the key of children as the attribute and the increasing number as the attribute value, for example

children = [{
    key: 'key1'
}, {
    key: 'key2'
}]
// Final map generated
map = {
    key1: 0,
    key2: 1,
}

So oldKeyToIdx is the hash table corresponding to the key of the old vnode
See whether the corresponding oldVnode can be found according to the key of newStartVnode

  • If the oldVnode does not exist, a new node is created and the newStartVnode moves to the right
  • If a node is found:

    • And it's similar to newStartVnode. The value of the location in the map table is undefined (to ensure that the key is unique). At the same time, insert newStartVnode.elm in front of oldStartVnode.elm, and then index moves backward one bit
    • If sameVnode is not met, only one new node can be created and inserted into the child node of parentElm, and newStartIdx moves backward one bit
  1. After the end of the cycle

    • If the oldStartIdx is larger than the oldEndIdx, the nodes without comparison in the new node will be added to the end of the team
    • If newstartidx > newendidx, there are still new nodes. Delete these nodes

summary

This article explains how views are updated when data changes. Some details are omitted. If you need to know more, it is more appropriate to combine source code. My github Please pay more attention, thank you!

Posted by Averice on Tue, 19 Nov 2019 05:24:02 -0800