What is the relationship between VirtualDom and Path?

VNode (VirtualDom)

Before bidirectional binding, we needed to manipulate the DOM nodes directly in each of the trigger event methods to modify the corresponding view. However, when a large application becomes difficult to maintain, reflow can affect performance.

Therefore, some people propose that we can abstract the real DOM tree into an abstract tree made of JavaScript objects, and after modifying the abstract tree data, convert the abstract tree into the real DOM and redraw it on the page. The virtual DOM emerges, which is an abstraction of the real DOM and uses properties to describe various features of the real DOM. When it changes, it changes the view.

As you can imagine, if the simplest and most crude way to modify the entire DOM structure to the page is to use innerHTML, then redrawing the entire view layer in this way would be very performance costly. Would you consider updating only the modified part of the view at a time? Therefore, VUe. js abstracts DOM into a virtual DOM tree with JavaScript objects as nodes. VNode nodes are used to simulate the real DOM. Operations such as node creation, node deletion and node modification can be carried out on this abstract tree without the need to operate the real DOM. You only need to manipulate the JavaScript object and then only modify the differences, which is a big performance boost compared to the rough modification of the entire innerHTML. After modification, diff algorithm is used to obtain some minimum units to be modified, and then the view of these small units is updated. Doing so reduces the number of unwanted DOM operations and greatly improves performance.

Vue uses such an abstract node, VNode, which is a layer of abstraction of the real DOM, and does not depend on a platform. It can be a browser platform, or weeX, and even node platform can create and delete such operations on such an abstract DOM tree, which also provides the possibility of front and back end isomorphism.

Specific VNode details can see Vue source code (seven) – render to VNode generation.

How do I modify the view?

As you’ve already seen, Vue uses data binding to modify views. When data is modified, the set method makes the notify call to Dep in the closure notify all subscribers Watcher. Watcher executes vm._update(vm._render(), hydrating) via get.

Vue.prototype._update = function(vnode: VNode, hydrating? : Boolean) {const VM: Component = this /* If the Component has already been mounted then entering this step is an update process, triggering beforeUpdate hook */if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected inEntry points // based on the rendering backend used. /* Vue. Prototypeif(! prevVnode) { // initial render vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el__patch__(prevVnode, vnode)} activeInstance = prevActiveInstance // update __vue__ reference /* Update the __vue__*/ of the new instance objectif (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook. }Copy the code

The first argument to the update method is a VNode object that is internally __patch_ with the previous old VNode object.

So what exactly is path?

path

Patch compares the old and new VNodes and then modifies the view in the smallest unit based on the comparison results, rather than redrawing the entire view according to the new VNode. The core of Patch lies in diff algorithm, which can efficiently compare changes of virtual DOM and obtain changes to modify views.

So how does Patch work?

Firstly, the core diff algorithm of Patch is discussed. Diff algorithm compares tree nodes of the same layer instead of searching and traversing the tree layer by layer. Therefore, it is a fairly efficient algorithm with only O(n) time complexity.

These two diagrams represent the patch process between the old VNode and the new VNode. They only compare vnodes at the same level to get changes (the squares of the same color in the second diagram represent vNodes that are compared with each other), and then modify the changed view, so it is very efficient.

From the previous introduction, we know that we need to convert the VNode into a real DOMe node through the patch function:

vm.$el = vm.__patch__(prevVnode, vnode)
Copy the code

And __patch__ is in platforms/web/runtime/index defined in js:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
Copy the code

The main purpose here is to determine whether the current environment is in the browser environment, that is, whether the Window object exists. For cross-platform processing, patch is an empty operation in the Server Render environment. Path (SRC /core/vdom/patch.js)

/* Return value of createPatchFunction, a patch function */return functionPatch (oldVnode, vnode, hydrating, removeOnly) {/* If the vnode does not exist, call the destruction hook */if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if(isUndef(oldVnode)) {// empty mount (likely as Component), create new root element /*oldVnode Create a new node */ isInitialPatch =true
      createElm(vnode, insertedVnodeQueue)
    } else{/* Mark old vnodes with nodeType*/ const isRealElement = isDef(oldvNode.nodeType)if(! IsRealElement && sameVnode(oldVnode, vnode) {// Patch existing root node /* patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) }else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if(oldvnode.nodetype === 1&&oldvnode.hasattribute (SSR_ATTR)) {/* when the oldVnode is a server rendered element, hydrating is denoted astrue*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if(isTrue(hydrating)) {/* Need to merge into the real DOM */if(oldVnode, vnode, insertedVnodeQueue) {/* Call insert hook */ invokeInsertHook(vnode, insertedVnodeQueue,true)
              return oldVnode
            } else if(process.env.NODE_ENV ! = ='production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.') } } // either not server-rendered, Or encounter failed. // Create an empty node and replace it /* If not server rendering or merging to the real DOM fails, Replacing it with an empty VNode */ oldVnode = emptyNodeAt(oldVnode)} // replacing existing element */ const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edgecase: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (# 4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if(isDef(vnode.parent)) {/* The component root node is replaced, iterating to update the parent element*/let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if(Patchable) {/* Call create callback */for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // # 6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if(isDef(parentElm)) {/* Remove old nodes */ removeVnodes(parentElm, [oldVnode], 0, 0)}else if(isDef(oldvNode.tag)) {/* Call destroy hook */ invokeDestroyHook(oldVnode)}} /* Call insert hook */ invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm
  }
Copy the code

The createPatchFunction is used to create and return a patch function. Path accepts six parameters:

1. OldVnode: old virtual node or old real DOM node

2. Vnode: indicates a new virtual node

3. Hydrating: Whether to merge with the real DOM

4. RemoveOnly: special flag for components

5. ParentElm: the parent node

6. RefElm: The new node will be inserted before refElm

Apart from calling the lifecycle hooks and destroying the nodes, the key elements in the code are the sameVnode, createElm, and patchVnode methods.

sameVnode

Let’s look at the sameVnode implementation.

/* To check whether two VNodes are the same node, the following conditions must be met: Key Same tag (tag name of the current node) same isComment (comment node) Same Data (object corresponding to the current node, contains specific data information, is a VNodeData type, When the tag <input>,typeMust be the same */function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

// Some browsers do not support dynamically changing type for<input> // so they need to be treated as different nodes /typeAre they the same Some browsers do not support dynamic modification of <input> types, so they are treated as different types */function sameInputType (a, b) {
  if(a.tag ! = ='input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
Copy the code

createElm

function createElm (vnode,insertedVnodeQueue,parentElm, refElm,nested,ownerArray,index) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(VNode)} // Used to create the component, initialize the component, and reactivate the component after calling the component initialization hook. DOM vNode. isRootInsert =! nested //for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if(isDef(tag)) {// Error detection is used to determine whether component is properly registeredif(process.env.NODE_ENV ! = ='production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.', vnode.context)}} // nodeOps encapsulates a collection of dom operations vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)set/* Istanbul ignoreif* /if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if(! appendAsTree) {if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } elseNodeops.appendchild (...) {// Create a child node, if the child node is an array, then execute createElm. Insert text content into the real DOM. createChildren(vnode, children, insertedVnodeQueue)if(isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)} // Insert into the real DOM refElm) }if(process.env.NODE_ENV ! = ='production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if(isTrue(vnode.iscomment)) {vnode.elm = nodeops.createcomment (vnode.text) insert(parentElm, vnode.elm, refElm)}else { // 文本
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }
Copy the code

As you can see from the above comments, the purpose of the createElm method is to create a real DOM object

patchVnode

Let’s take a look at the patchVnode code first.

/*patch VNode */functionPatchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {/* If two vNodes are the same, */ is returnedif (oldVnode === vnode) {
      return
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      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/* If both the old and new vNodes are static, their keys are the same, and the new VNode iscloneOr once (mark the V-once attribute and render only once), then only elm and componentInstance need to be replaced. * /if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if(isDef(data) &&isdef (I = data.hook) &&isdef (I = i.patch)) {/* I = data.hook"./create-component componentVNodeHooks". */ i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.childrenif(isDef(data) &&isPatchable (vnode)) {/* Call 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.pdate)) I (oldVnode, vnode)} /* If the vnode has no text */if (isUndef(vnode.text)) {
      if(isDef(oldCh) &&isdef (ch)) {/* If the old and new nodes have children, diff the children and call updateChildren*/if(oldCh ! == ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) }else if(isDef(ch)) {/* If the old node has no children and the new node has children, clear the text of elm first and add children */ to the current nodeif (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ' ')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if(isDef(oldCh)) {/* Remove all ele children */ removeVnodes(elm, oldCh, 0, oldch.length-1)}else if(isDef(oldvnode.text)) {/* Insert text */ nodeops.settextContent (elm,' ')}}else if(oldVnode.text ! */ nodeops.settextContent (elm, vnode.text)} /* Call postPatch hook */if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
Copy the code

The rules of patchVnode are as follows:

1. If the old and new VNodes are static, have the same key (representing the same node), and the new VNode is clone or once (marked with the V-once attribute, rendering only once), then only elm and componentInstance need to be replaced.

2. If both the old and new nodes have children, diff is performed on the children and updateChildren is called, which is also the core of the diff.

3. If the old node has no children and the new node has children, clear the text content of the DOM of the old node and add children to the current DOM node.

4. When a new node has no children and an old node has children, remove all children of the DOM node.

5. When the old and new nodes have no children, it is just a text replacement.

updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    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, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay incorrect relative positions // during leaving transitions const canMove = ! removeOnlyif(process.env.NODE_ENV ! = ='production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if(sameVnode(oldStartVnode, newStartVnode)) {/* the first four cases are considered to be the sameVnode when the key is specified, so patchVnode can be used directly. */ 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)) { // Vnode moved right
        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)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else{/* Create a hash table with a key corresponding to the old VNode key (this is only generated when undefined for the first time, which is also used to detect duplicate keys later). Childre looks like this.'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}} beginIdx = 0 endIdx = 2 result generated {key0:0, key1:1, key2:2} */if(isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, OldEndIdx) /* If the new VNode newStartVnode has a key and the key can be found in oldVnode, return the idxInOld of the node. */ idxInOld = isDef(newstartvNode.key)? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if(isUndef(idxInOld)) {// New element /*newStartVnode has no key, or the key is not found in the old node to create a New node */ createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm,false, newCh, newStartIdx)
        } else{/* Obtain the old node with the same key */ vnodeToMove = oldCh[idxInOld]if(sameVnode(vnodeToMove, newStartVnode)) {/* If the new VNode is the same as the node with the same key, patchVnode*/ patchVnode(vnodeToMove, NewStartVnode, insertedVnodeQueue) /* Set patchVnode to undefined, OldCh [idxInOld] = undefined /* When there is an identifier canMove can be directly inserted in front of the real DOM node corresponding to oldStartVnode */ canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) }else{// Same key but different element. Treat as new element /* When the new VNode is not a sameVNode with the same key (such as a different tag or a different one)typeCreateElm (newStartVnode, insertedVnodeQueue, parentElm, oldStartvNode.elm,false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    ifOldStartIdx > oldEndIdx {/* oldStartIdx > oldEndIdx {/* oldStartIdx > oldEndIdx; */ refElm = isUndef(newCh[newEndIdx + 1])? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) }else if(newStartIdx > newEndIdx) {/* If newStartIdx > newEndIdx is found after all comparisons are completed, the new node has been traversed, and the old node has more new nodes. */ removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)}}Copy the code

Let’s draw a picture of the general process:

Define an initial variable:

letOldStartIdx = 0 // Old list starting positionletNewStartIdx = 0 // Start position of new listletOldEndIdx = oldch.length - 1 // End of the old listletOldStartVnode = oldCh[0] // Start value of the old listletOldEndVnode = oldCh[oldEndIdx] // End of the old listletNewEndIdx = newch.length-1 // End of the new listletNewStartVnode = newCh[0] // New list start valueletNewEndVnode = newCh[newEndIdx] // New list end valueCopy the code

First, there is a variable mark on both sides of the left and right sides of the old and new VNodes, which converge to the middle during traversal. The loop ends when oldStartIdx > oldEndIdx or newStartIdx > newEndIdx.

Mapping between indexes and VNodes: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode

In traversal, if a key is present and sameVnode is satisfied, the DOM node is reused, otherwise a new DOM node is created.

First, oldStartVnode, oldEndVnode, newStartVnode, newEndVnode pairwise comparison, there are 2*2=4 comparison methods.

When the start or end of the new and old vNodes meets sameVnode, that is, sameVnode(oldStartVnode, newStartVnode) or sameVnode(oldEndVnode, newEndVnode), Directly patchVnode the VNode.

At this time, oldStartVnode has run after oldEndVnode, and the real DOM node needs to be moved to the back of oldEndVnode while patchVnode is being performed.

This indicates that oldEndVnode runs in front of oldStartVnode, and the real DOM node moves in front of oldStartVnode when patchVnode is performed.

1. When oldStartIdx > oldEndIdx ends, the old VNode has been traversed, but the new node has not. Insert the remaining vNodes into the real DOM. Call addVnodes (createElm) to add these nodes to the real DOM.

conclusion

Here, the main functions of patch are basically finished. We find that a key field appears in large numbers in this paper. After the above investigation, we already know that the core of Vue’s DIff algorithm is based on two simple assumptions:

1. Two identical components produce similar DOM structures, and different components produce different DOM structures

Based on the above two assumptions, the complexity of the Diff algorithm of the virtual DOM is reduced from O(n^3) to O(n). When the data of the page changes, the Diff algorithm will only compare nodes of the same level:

If you are interested in diff, I recommend reading this article to delve into vue 2.x’s virtual DOM diff principles

Thanks for the material provided by Teacher Yenmo and Muwoo.

If you like, you can give me a star, Github