I. Introduction to Virtual DOM

Real DOM

Virtual Virtual DOM

To solve the performance problem of frequently manipulating the DOM, the Virtual DOM was conceived. The Virtual DOM uses a native JS object to describe a DOM node. It is therefore much less expensive than creating a real DOM.

Ii. The process from Virtual DOM to real DOM (Vue)

1. Define a VNode

In Vue, a VNode is a Virtual DOM generated by calling the Render Function. It is a JavaScript object that uses object properties to describe the node. This is essentially a layer of encapsulation of the real DOM. Virtual DOM performance is good, thanks to the js execution speed. A series of complex DOM operations such as creating nodes, deleting nodes and modifying nodes are all handed over to Virtual DOM. This greatly improves performance over rough rearranging and redrawing pages using JS innerHTML.

Properties of a VNode object

The VNode object defines the following attributes:

In the SRC/core/vdom vnode. Js file

exportdefault class VNode { tag: string | void; data: VNodeData | void; children: ? Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessaryfor enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm forfunctional nodes fnOptions: ? ComponentOptions; //forSSR caching devtoolsMeta: ? Object; // used to store functional render contextfordevtools fnScopeId: ? string; // functional scope id support constructor ( tag? : string, data? : VNodeData, children? :? Array<VNode>, text? : string, elm? : Node, context? : Component, componentOptions? : VNodeComponentOptions, asyncFactory? : Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw =false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
Copy the code

A VNode object contains the following properties:

  • Tag: indicates the tag name of the current node
  • data: Data object of the current node. Refer to the definition of VNodeData in types/ vNode.d. ts in the Vue source code for specific fields
  • Children: Array type, children of the current node
  • Text: indicates the text of the current node
  • Elm: real on the current virtual node
  • Ns: namespace of the current node
  • Context: compilation scope of the current node
  • Key: the key attribute of a node, which is used as the identification of a node, facilitating patch optimization
  • ComponentOptions: Information about the options used to create component instances
  • ComponentInstance: componentInstance corresponding to the current node
  • Parent: indicates the parent of the current node
  • Raw: Determines whether it is HTML or plain text. True for innerHTML and false for innerText
  • IsStatic: indicates the id of a static node
  • IsRootInsert: whether to insert as root, wrapped node. This property has a value of false
  • IsComment: Whether the current node is a comment node
  • Isempirical: Whether the current node is a clone node
  • IsOnce: Whether there is a V-once command…

VNode classification

  • EmptyNode: Comment node with no content
  • TextVNode: text node
  • ElementVNode: Common element node
  • ComponentVNode: component node
  • CloneVNode: a clone node, having any of the above types, the only difference being that the empirical attribute of isverification is true…

2, the createELement method

SRC/core/vdom/create – element. The js file

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export functioncreateElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: Boolean) : VNode | Array < VNode > {/ / compatible with not sending dataif (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if(isTrue(alwaysNormalize)) {normalizationType = ALWAYS_NORMALIZE} // Call _createElement to create a virtual nodereturn _createElement(context, tag, data, children, normalizationType)
}

export function_createElement ( context: Component, tag? : string | Class<Component> | Function | Object, data? : VNodeData, children? : any, normalizationType? : number) : VNode | Array < VNode > {/ / judge whether __ob__ response data, do not allow the VNode is response dataif(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
      context
    )
    returnCreateEmptyVNode () returns a comment node} // object syntaxin v-bind
  if(isDef(data) &&isdef (data.is)) {tag = data.is} // When the component's IS attribute is set to falsy's value // create a comment node without contentif(! tag) {return createEmptyVNode()
  }
  
  // warn against non-primitive key
  if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: Children [0]} children.length = 0} // Select different processing methods according to the value of normalizationTypeif(normalizationType === ALWAYS_NORMALIZE) {children = normalizeChildren(children)else if(normalizationType === SIMPLE_NORMALIZE) {children = simpleNormalizeChildren(children)letVnode, ns // Check whether the tag is a stringif (typeof tag === 'string') {
    letCtor // Configure the namespace for the label name ns = (context.$vnode && context.$vnodeNs) | | config. GetTagNamespace (tag) / / judge whether the tag is preservation of the HTML tagif(config.isreservedtag (tag)) {// isReservedTag, Create keep tabs VNode VNode = new VNode (config. ParsePlatformTagName (tag), data, children, undefined, undefined, Context) // Check if tag is a component}else if((! data || ! data.pre) && isDef(Ctor = resolveAsset(context.$options.'components', tag))) {// Is the component tag to create a componentVNode vNode = createComponent(Ctor, Data, Context, Children, Tag)}elseVnode = new vnode (tag, data, children, undefined, undefined, context)}}else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
Copy the code

The createElement logic can be summarized as follows:

3, the update

Vue _update is a private method that is called at two times, one for first rendering and one for data update. Let’s take a look at the first render, which calls the updateComponent method as follows:

In the SRC/core/instance/lifecycle. Js file

updateComponent = () => {
    vm._update(vm._render(), hydrating)
}
Copy the code
Vue.prototype._update = function(vnode: VNode, hydrating? : boolean) { const vm: Component = this const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected inEntry points // Based on the rendering backend used. // If the prevVnode that requires diff does not exist, create a real DOM node with the new vNodeif(! prevVnode) { // initial render //$elThe parameter is the actual DOM node VM.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else{// updates // prevVnode exists, passing the prevVnode and vNode to diff, completing the working VM of updating the real DOM.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (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 _update method calls a core method __patch__, which can be said to be the core method of the entire Virtual DOM to build the real DOM. It mainly completes the diff process of the new virtual node and the old virtual node, generates the real DOM node and completes the view update after the patch process.

4, Patch

The __Patch__ method compares old and new VNodes and then changes the view in minimal units based on the result of the comparison. The core of Patch lies in the DIff algorithm, which can compare VNode changes efficiently.

The diff algorithm

Let’s take a look at the diff algorithm. This algorithm compares nodes in the same layer of the tree rather than searching through the tree layer by layer, so the time complexity is only O(n), and the performance is quite efficient.


SRC/core/vdom/patch. Js file

return functionPatch (oldVnode, vNode, hydrating, removeOnly) {// If the vnode does not exist, call the destroy hook directlyif (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 // Create a new node isInitialPatch =true
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if(! IsRealElement && sameVnode(oldVnode, vnode) {// Patch existing root node // Modify patchVnode(oldVnode, vnode, insertedVnodeQueue, NULL, NULL, 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, the hydrating flag istrue
          oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if(isTrue(hydrating)) {// need to merge into the real DOMif(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-side rendering or merging to the real DOM fails, OldVnode = emptyNodeAt(oldVnode)} // Replacing existing element const oldElm = oldvNode. elm const parentElm = 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 over the parent elementlet 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) {
              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

It is not difficult to find from the patch code that patchVnode will be called only when oldVnode and Vnode are in the same node as sameVnode; otherwise, a new DOM will be created and the old DOM will be removed. The rules of patchVnode are as follows: 1) If oldVnode and VNode are exactly the same, nothing needs to be done. 2) If oldVnode and vnode are static nodes with the same key, when vnode is a clone or a V-once node, just copy oldvNode. elm and oldvNode. child to vnode and no other operations are required. 3) 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. 4) When the old node has no children and the new node has children, first empty the text content of the DOM of the old node, and then add children to the current DOM node. 5) When the new node has no children and the old node has children, remove all children of the DOM node directly. 6) When the old and new nodes have no children, it is just a text replacement. The updateChildren function is the core of diff.

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 ifSameVnode (oldStartVnode, newStartVnode) {// if oldStartVnode and newStartVnode are the sameVnode, Recursive patchVnode patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, NewStartIdx) oldStartVnode = oldCh[++oldStartIdx]// oldStartIdx move right newStartVnode = newCh[++newStartIdx]// NewStartIdx move right}else if(sameVnode(oldEndVnode, newEndVnode)) {// if oldEndVnode, newEndVnode are the sameVnode, PatchVnode (oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, NewEndIdx) oldEndVnode = oldCh[--oldEndIdx]// oldEndIdx left move newEndVnode = newCh[--newEndIdx]// newEndIdx left move}else if// Vnode moved right // if oldStartVnode and newEndVnode are the sameVnode, PatchVnode (oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, Nodeops.nextsibling (oldEndVNode.elm)) oldStartVnode = oldCh[++oldStartIdx]// oldStartIdx move right newEndVnode = NewCh [--newEndIdx]// newEndIdx move left}else if// Vnode moved left // if oldEndVnode and newStartVnode are the sameVnode, PatchVnode (oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, Elm) oldEndVnode = oldCh[--oldEndIdx]// oldEndIdx move to the right newStartVnode = newCh[++newStartIdx]// NewStartIdx move left}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, newCh, newStartIdx) oldCh[idxInOld] = undefined 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

You are still a little confused by the idea of the updateChildren algorithm.

  • 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. OldStartVnode and oldEndVnode are compared with newStartVnode and newEndVnode in pairs. 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.
    • If oldStartIdx and newEndIdx satisfy sameVnode, that is sameVnode(oldStartVnode, newEndVnode). 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.
    • If oldEndIdx and newStartIdx satisfy sameVnode, i.e. SameVnode (oldEndVnode, newStartVnode). This indicates that oldEndVnode runs in front of oldStartVnode, and the real DOM node moves in front of oldStartVnode when patchVnode is performed.
  • If none of the above is true, createKeyToOldIdx yields an oldKeyToIdx containing a hash table containing the old VNode key and the corresponding index sequence. This hash table can be used to find whether there are old vNodes with the same key as newStartVnode. If both sameVnode and newStartVnode satisfy, PatchVnode moves the real DOM (elmToMove) in front of the real DOM corresponding to oldStartVnode.
    • It is also possible that newStartVnode cannot find the same key in the old VNode, or that it is not sameVnode even though the key is the same, in which case createElm will be called to create a new DOM node.
  • At this point the loop is over, so we’re left with extra or insufficient real DOM nodes to deal with.
    • 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.
    • Similarly, when newStartIdx > newEndIdx, the new vNodes have been traversed, but the old nodes are still available. This indicates that the real DOM nodes are redundant and need to be deleted from the document. In this case, call removeVnodes to remove these redundant real DOM nodes.

summary

The Virtual DOM goes through several key steps: createElement generates a VNode, update the view, patch compares the old and new Virtual nodes, and creates a DOM element. The patch function compares the old and new VNodes and adopts the DIff algorithm. The algorithm idea comes from SNabbDOM. If you are interested, you can further study the snabbDOM source code