Virtual DOM and diff algorithm in Vue

Posted by claire on Sat, 24 Aug 2019 07:19:18 +0200

Virtual dom

  • Why did it happen?

    Browsers parse an html in five steps: create DOM tree -> create Style Rules -> build Render tree -> layout Layout -> draw Painting. Every time a real dom is manipulated, the browser executes the process from start to finish by building the DOM tree. Real dom operation is expensive, frequent operation will cause page carton to affect user experience, virtual dom is created to solve this browser performance problem.

    Virtual DOM does not directly manipulate the real DOM after executing the update operation of dom, but saves the updated diff content to the local js object, then attach es it to the DOM tree at one time, informing the browser to draw the DOM to avoid a large number of meaningless calculations.

  • How to achieve:

    js objects represent the dom structure, which records the labels, attributes and sub-nodes of dom nodes

    render function of js object constructs a real dom tree by recursion of attributes and sub-nodes of virtual dom

    Virtual DOM is a pure JS object that can be created through document. createDocument Fragment. A virtual DOM in Vue contains the following attributes:

    • Tag: The tag name of the current node
    • data: data object of the current node
    • children: Array type, containing the child nodes of the current node
    • Text: The text of the current node, which is the property of a general text node or comment node
    • elm: The real dom node corresponding to the current virtual node
    • context: Compile scope
    • Functional Context: Scope of Functionalized Components
    • Key: The key attribute of the node, which is used as the identifier of the node, is beneficial to the optimization of the patch.
    • sel: node selector
    • CompoonentOptions: Options information used to create component instances
    • child: A component instance corresponding to the current node
    • parent: The placeholder node of the component
    • raw: raw html
    • isStatic: Identification of Static Nodes
    • isRootInsert: Whether to insert the wrapped node as the root node, the value of this attribute is false
    • isComment: Is the current node a comment node?
    • isCloned: Is the current node cloned?
    • isOnce: Does the current node have a v-once instruction

Summary: Virtual DOM simulates real DOM nodes with JavaScript, compares DOM changes and puts them into the Js layer.

  • Advantages:

    • Cross-platform: Virtual DOM is based on JavaScript objects and does not depend on the real platform environment, so it has cross-platform capabilities, such as browser platform, Weex, Node and so on.
    • Improve DOM operation efficiency: DOM operation execution speed is far less than Javascript operation speed, therefore, a large number of DOM operations are moved to Javascript, using patching Algorithm to calculate the nodes that really need to be updated, minimizing DOM operation, thus significantly improving performance.
    • Improving rendering performance: With a large number of frequent data updates, depending on diff algorithm, the view can be updated reasonably and efficiently.

The process of converting template into view in vue

  • Vue.js translates template template into render function by compiling, and executes render function to get a virtual node tree.
  • When operating on Model, the Watcher object in Dep is triggered. The Watcher object calls the corresponding update to modify the view. This process mainly compares the difference between the old and the new virtual nodes (patch), and then updates the view by DOM operation based on the comparison results.

diff algorithm is an optimization method, which compares the difference between the two modules. The process of repairing (updating) the difference is called patch.

patch:

The core part of virtual DOM is that it can render vnode into real DOM. This process is to compare the differences between new and old virtual nodes, and then find out the nodes that need to be updated according to the comparison results.

patch itself has the meaning of patching and patching. Its actual function is to update the view by modifying the existing DOM. Vue's Virtual DOM patching algorithm is based on the implementation of Snabbdom, and has made a lot of adjustments and improvements.

diff flow chart

When the data changes, the set method will call Dep.notify to notify all subscribers of Watcher, and the subscriber will call patch to patch the real DOM and update the corresponding view.

The diff algorithm of Vue is to do diff only between vnodes of the same level, recursively diff the same level vnode, and finally update the whole DOM tree. Because cross-level operations are very few and neglected, the time complexity changes from O(n3) to O(n).


Hypothesis of diff algorithm

  • In Web UI, DOM nodes have very few cross-level mobile operations, which can be neglected.
  • Two components with the same class will generate similar tree structure, and two components with different classes will generate different tree structure.
  • For a group of sub-nodes at the same level, they can be distinguished by a unique id.

patch process

When the key s and sel s of the new and old virtual nodes are the same, the depth patch of the node is carried out. If not, the virtual nodes are replaced as a whole, and the real DOM is created to update the view.

How to determine whether the old and new nodes are the same node?

When the tag, key and isComment of two VNodes are identical, and data is defined or not defined at the same time, and if the tag is input, the type must be the same. At this time, these two VNodes are sameVnode, which can be directly operated on by patchVnode.

function patch (oldVnode, vnode) {
    if (sameVnode(oldVnode, vnode)) { // It's necessary to do patch when both key and sel are the same.
        patchVnode(oldVnode, vnode)
    } else {  // There's no need to patch, the whole replacement
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode) // Vnode creates its real dom, so vnode.el = real DOM
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // Insert the entire new node tree
            api.removeChild(parentEle, oldVnode.el) // Remove the entire old virtual DOM
            oldVnode = null
        }
    }
    return vnode
}

Deep patch:

 function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*If two VNode nodes are the same, they return directly*/
    if (oldVnode === vnode) {
      return
    }
    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    /*
      If both old and new VNode s are static and their key s are the same (representing the same node),
      And the new VNode is clone or once (marking the v-once attribute, rendering only once).
      All you need to do is replace elm and component Instance.
    */
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      /*i = data.hook.prepatch,If it exists, see ". / create-component component component VNodeHooks".*/
      i(oldVnode, vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      /*Call the update callback and update hook*/
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    /*If the VNode node does not have text text*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        /*If both old and new nodes have child child subnodes, the child node is diff operated and updateChildren is called.*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        /*If the old node has no sub-nodes and the new node has sub-nodes, first empty the text content of elm, and then add sub-nodes to the current node.*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        /*When the new node has no children and the old node has children, all ele's children are removed.*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        /*When the old and new nodes have no sub-nodes, they just replace the text, because the new node text does not exist in this logic, so the text of ele is removed directly.*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      /*When the old and new node texts are different, replace the text directly*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*Call the postpatch hook*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patchVnode Rules

1. If the old and new VNode s are static and their key s are the same (representing the same node), then only the elm and component Instance need to be replaced (in-place reuse).

2. New and old nodes have child child child child child child child child nodes, and if they are different, they will diff the child nodes and call updateChildren, which is also the core of diff.

3. If only the new node has child nodes, first empty the text content of the old node DOM, and then add child nodes for the current DOM node.

4. If only the old node has children, delete the children of the old node directly.

5. When the old and new nodes have no child nodes, they are just text replacements.

updateChildren

Next comes the understanding of the most complex diff algorithm

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, 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
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {   // For vnode.key comparison, oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // A comparison of key s
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // Generate index table with key
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
            }
            else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

Overview of the process

  • Extract the child nodes of Vnode Vch and oldVnode oldCh
  • Old Ch and vCh have two variables, StartIdx and EndIdx, which are compared with each other in four ways. When two of them can match, the corresponding node in the real dom will move to the corresponding position of Vnode. If none of the four comparisons matches, if key is set, the comparison will be made with key. In the process of comparison, variables will lean to the middle. Once StartIdx > EndIdx indicates that at least one of the oldCh and vCh has been traversed, the comparison will be ended.

There is a variable marker on the left and right sides of the new and old VNode nodes, and these variables will move towards the middle during traversal. End the loop when oldStartIdx <= oldEndIdx or newStartIdx <= newEndIdx.

We use an example to understand the whole process of comparison:

Real Nodes: a, b, d

Old nodes: a, b, d

New nodes: a, c, d, b

Step 1:

oldS = a, oldE = d;
S = a, E = b;

Old S is compared with S, E; Old E is compared with S, E, and the conclusion that Old S and S match is drawn, so a node should be placed first in the order of new nodes. At this time, the old node's a node is also the first one, so the position of the old node does not move.

At the end of the first round of comparison, oldS and S are the same node, moving backwards, and oldE and E are fixed.

Step 2:

Old nodes: a, b, d

New nodes: a, c, d, b

oldS = b, oldE = d;
S = c, E = b;

Four variables can be matched by two vehicles to get oldS and E matching, moving the original b node to the end, because E is the last node, and their positions should be the same. This is what was said above: when two of them can match so well, the corresponding nodes in the real dom will move to the corresponding position of Vnode;

At the end of the second round of comparison, oldE and E are the same node, moving forward, and oldS and S are fixed.

Step 3:

Old nodes: a, d, b

New nodes: a, c, d, b

oldS = d, oldE = d;
S = c, E = d;

Old E and E match, position unchanged;

Step 4:

Old nodes: a, d, b

New nodes: a, c, d, b

oldS++;
oldE--;
oldS > oldE;

The end of traversal indicates that the old node traverses first. Insert the remaining new node c into the real dom according to its index

Old nodes: a, c, d, b

New nodes: a, c, d, b

Complete the comparison.

Of course, there are also four variables that can not match each other, divided into two cases

  • If key exists in both old and new sub-nodes, a hash table will be generated according to the key of the old node. If the key of S matches the hash table, the matching success will determine whether S and the matching node are sameNode. If so, move the successful node to the front of the real dom. Otherwise, insert the corresponding node generated by S into the dom. The oldS position, oldS and S pointer move to the middle.
  • If there is no key, then directly insert S to generate new nodes into the real DOM (here you can explain why setting key makes diff more efficient

There are two specific situations at the end:

  1. Old S > Old E, it can be considered that the old node traverses first. Of course, it is also possible that the new node just completed the traversal at this time, and the unification is classified as this kind. At this point, the vnode between S and E is newly added. Call addVnodes and insert all the virtual nodes. elms behind before.
  2. S > E, you can think that the new node traverses first. At this point, the node between oldS and oldE no longer exists in the new child node, so delete it directly.

Experiencing in simulation of two examples

eg.1
O b,a,d,f,e
N a,b,e
1. 
oldS = b, oldE = e;
S = a, E = e;
O b,a,d,f,e
N a,b,e
2.
oldS = b, oldE = f;
S = a, E = b;
O a,d,f,b,e
N a,b,e
3.
s>e d,f delete
O a,b,e
N a,b,e

eg.2
O b,d,c,a
N a,e,b,f
1. 
oldS = b, oldE = a;
S = a, E = f;
O a,b,d,c
N a,e,b,f
2.
oldS = d, oldE = c;
S = e, E = f;
//At this time, the four parameters can not match, according to the key to compare whether there are S-corresponding nodes in O, and if not, insert the corresponding nodes in the S-position of O.
O a,e,d,b,c
N a,e,b,f
3.
oldS = d, oldE = c;
S = b, E = f;
//At this time, the four parameters can not match, according to the key to find whether there is a corresponding B node S, yes, move to the current position of S.
O a,e,b,d,c
N a,e,b,f
4.
oldS = d, oldE = c;
S = f, E = f;
//At this time, the four parameters can not match, according to the key to find whether there is a corresponding f node S, no, then insert the corresponding node in the S position of O.
O a,e,b,d,c,f
N a,e,b,f
5.
oldS = d, oldE = c;
s>f
//When the loop ends, the nodes between oldS and oldE are deleted


Summary:

  • Try not to modify dom across levels
  • When developing components, maintaining a stable DOM structure can help improve performance
  • Setting key makes diff more efficient

Reference resources:

Explain Virtual DOM in Vue

Virtual DOM and diff(Vue implementation)

Introduction to Virtual DOM

Data Update to View Update, What does Vue do

The diff algorithm of vue in detail

Topics: Javascript Vue Attribute Fragment