First published at: github.com/USTB-musion…

Writing in the front

We are familiar with the concept of Virtual DOM and Vritual DOM is relative to DOM(document Object Model). The definition of DOM in MDN is as follows: “The DOM model represents a document with a logical tree. Each branch of the tree ends in a node, and each node contains objects. The DOM’s methods let you manipulate the tree in ways that change the structure, style, or content of the document. Compared to the performance problems caused by frequent manipulation of DOM, Vritual DOM makes a good mapping of DOM, mapping a series of operations that originally needed to be performed on DOM to Virtual DOM.

The “expensive” DOM

To get a better feel for the “expensive” DOM, now print out all the attribute values for a simple DIV element:

let div = document.createElement('div')
let str = ' '
for (let key in div) {
	str += key + ' '
}
Copy the code

The printed STR value is:

As you can see, the actual DOM elements are very large, and because browsers have designed the DOM to be very complex, there are performance issues when we update the DOM frequently. As you can imagine, rewriting the entire DOM structure onto the page with innerHTML in a very simple and crude way can be very performance consuming to redraw the entire view layer. So when we update the DOM, can we just update where we changed it?

VNode

As we know, after going through the Render function we get the VNode node. If you don’t understand this picture, take a look at the two articles I wroteVue.js source perspective: analysis of the template and data rendering into the final DOM processandThe responsive system principle of vue.jsA Vritual DOM is actually a layer of encapsulation of the real DOM, which is based on VNode nodes (JavaScript objects) and describes the nodes with object attributes. A Vritual DOM defines some of the key information about the real DOM, and Vritual DOM is implemented entirely in JS, with no connection to the host browser, and thanks to the speed of JS execution, A series of complex DOM operations, such as creating nodes, deleting nodes, and adding nodes, which need to be carried out in the real DOM, are all carried out in the Vritual DOM. This greatly improves performance compared to roughly redrawing the entire view with innerHTML. Using the diff algorithm to update only the modified Virtual DOM, this can avoid many unnecessary DOM changes and thus improve performance.

SRC /core/vdom/vnode.js

 export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  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; // necessary for 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 for functional nodesfnOptions: ? ComponentOptions;// for SSR cachingfnScopeId: ? 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

Among them:

Tag: indicates the tag name of the current node

Data: indicates the object corresponding to the current node, which contains specific data information. It is of the VNodeData type. For details, see the VNodeData type

Children: An array of children of the current node

Text: indicates the text of the current node

Elm: real DOM node corresponding to the current virtual node

Ns: indicates the namespace of the current node

Context: compilation scope of the current node

FunctionalContext: Functional component scope

Key: The key attribute of a node, which is used as a node identifier for optimization

ComponentOptions: Indicates the option of a component

ComponentInstance: componentInstance corresponding to the current node

Parent: indicates the parent of the current node

Raw: Simply means whether it is native HTML or plain text. True for innerHTML and false for textContent

IsStatic: indicates whether the node isStatic

IsRootInsert: Indicates whether to be inserted as the heel node

IsComment: Indicates whether it is a comment node

Isempirical: Whether there is a clone node

IsOnce: indicates whether the V-once command exists

For example, we now have a Vritual DOM like this:

 {
    tag: 'div'
    data: {
        class: 'outer'
    },
    children: [{tag: 'div'.data: {
                class: 'inner'
            }
            text: 'Virtual DOM'}}]Copy the code

The real DOM after rendering is:

<div class="outer">
    <span class="inner">Virtual DOM</span>
</div>
Copy the code

Create an empty VNode

export const createEmptyVNode = (text: string = ' ') = > {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
Copy the code

Create a text node

export function createTextVNode (val: string | number) {
  return new VNode(undefined.undefined.undefined.String(val))
}
Copy the code

Clone a VNode

export function cloneVNode (vnode: VNode) :VNode {
  const cloned = newVNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isComment = vnode.isComment cloned.fnContext = vnode.fnContext cloned.fnOptions = vnode.fnOptions cloned.fnScopeId = vnode.fnScopeId  cloned.asyncMeta = vnode.asyncMeta cloned.isCloned =true
  return cloned
}
Copy the code

In general, VNode is a JavaScript object, which uses JavaScript object attributes to describe some states of the current node, and uses the form of VNode nodes to simulate a Virtual DOM tree.

Update the view

As we know, vue.js updates views through data binding, which calls the updateComponent method. If you’re not sure about this process, take a look at the two articles mentioned above. The updateComponent method is defined as follows:

updateComponent = () = > {
 vm._update(vm._render(), hydrating)
}
Copy the code

. This method will be called the vm _update method, this method takes the first parameter is the newly generated VNode, defined in SRC/core/instance/lifecycle. In js:

  Vue.prototype._update = function (vnode: VNode, hydrating? : boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if(! prevVnode) {// initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)}else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // 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

Add a note in key places:

  / / new vnode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  // If the prevVnode that requires diff does not exist, create a real DOM node with the new vNode
  if(! prevVnode) {// initial render
   // The first parameter is the actual node
   vm.$el = vm.__patch__(
    vm.$el, vnode, hydrating, false /* removeOnly */,
    vm.$options._parentElm,
    vm.$options._refElm
   )
  } else {
   // updates
   // If the prevVnode for diff exists, first diff the prevVnode and vNode, patch the dom operations to the prevVnode, and complete the update of the real DOM
   vm.$el = vm.__patch__(prevVnode, vnode)
  }
Copy the code

It can be seen that this method calls a core method __patch__, which can be said to be the most core method of the entire Virtual DOM. It mainly completes the diff process of the new Virtual DOM node and the old Virtual DOM node. After the patch process, the real DOM node is generated and the view is updated.

patch

Let’s take a look at what happens to the vm.__patch__ method, defined in SRC /core/vdom/patch.js:

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    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
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      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)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              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 hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: 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)) {
          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) {
              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)) {
          removeVnodes(parentElm, [oldVnode], 0.0)}else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
Copy the code

Through the source code, we can find that patchVnode is only carried out when oldVnode(the old node) and Vnode(the new node) are in sameVnode, and the method sameVnode determines whether to diff and patch the oldVnode and Vnode. In other words, patchVnode will be performed only when the old and new VNodes are judged to be the same node; otherwise, new DOM will be created and old DOM will be removed. Here are the sameVnode methods:

sameVnode

SameVnode is defined in SRC /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(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

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

If the tag, key, and isComment of the old and new vNodes are the same, and data is defined or not, the type must be the same if the tag is input. At this point, the old and new vNodes are considered as Samevnodes, and then the patchVnode operation is performed.

The diff algorithm

Vue in 2.x vDOM algorithm is based on the snabbDOM algorithm modified implementation.

As shown in the figure, DIff algorithm compares tree nodes of the same layer instead of searching and traversing the tree layer by layer, so the time complexity is only O(n), which is a very efficient algorithm. Next, take a look at the implementation of updateChildren source code, the most important part of the DIff algorithm.

updateChildren

The updateChildren source code is defined in SRC /core/vdom/patch.js:

  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 in correct relative positions
    // during leaving transitions
    constcanMove = ! 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)) {
        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 {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        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)
    }
  }
Copy the code

This piece of source code analysis can refer to the SNabbDOM source code to learn.

You can pay attention to my public account “Muchen classmates”, goose factory code farmers, usually record some trivial bits, technology, life, perception, grow together.