patch analysis of vue virtual dom core method

Posted by Josien on Mon, 17 Jan 2022 07:42:30 +0100

Browser rendering engine process

Create DOM tree - > create StyleRules - > create Render tree - > Layout - > drawing

virtual Dom

First, virtual DOM is not unique to Vue, including React and other front-end frameworks. As long as the entity DOM is calculated before operation, it can be collectively referred to as virtual DOM based.

It is designed to solve the performance problem of browser.

snabbdom

Vue's virtual DOM technology is mainly based on snabbdom in GitHub:

snabbdom: a virtual DOM library that focuses on simplicity, modularity, power, and performance.

https://github.com/snabbdom/snabbdom

It can be said that we have understood the specific methods in snabbdom, that is, we have understood the core idea of virtual dom in vue:
When the status is updated, compare the new JavaScript object with the old JavaScript object (i.e. diff algorithm), calculate the difference between them, and apply the difference to the real DOM, so as to reduce the DOM operation.

diff strategy

  • Only compare the same level, not cross level
  • If the tag names are different, you can delete them directly without further depth comparison
  • If the tag name is the same and the key is the same, it is considered to be the same node. Do not continue the comparison

patch

patch method is the core of virtual dom. It will be called after vnode (virtual node) is changed and before initialization.

The main logic is as follows:
  • Call pre hook in module (life cycle related)
  • If an Element is passed in, it will be converted into an empty vnode (here, a dom Element will be passed in when it is first created)
  • If the old and new nodes are sameVnode, call patchVnode to update vnode, otherwise create a new node
  • Create new nodes, insert new nodes, and remove old nodes
  • Call insert hook on element (life cycle related)
  • Call post hook on element (life cycle related)
Source code analysis:
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
  let i: number, elm: Node, parent: Node;
  const insertedVnodeQueue: VNodeQueue = [];
  // Call pre hook in module
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

  // If an Element is passed in, it will be converted to an empty vnode
  if (!isVnode(oldVnode)) {
  //Create an empty vnode and associate the DOM element emptyNodeAt
    oldVnode = emptyNodeAt(oldVnode);
  }

  // patchVnode is called when sameVnode (sel and key are the same)
  // Judge whether the two parameters are the same vnode
  // Compare tag name key
  if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode, insertedVnodeQueue);
  } else {
    elm = oldVnode.elm as Node;
    parent = api.parentNode(elm);

    // Create a new dom node vnode elm
    createElm(vnode, insertedVnodeQueue);

    if (parent !== null) {
      // Insert dom
      api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
      // Remove old dom
      removeVnodes(parent, [oldVnode], 0, 0);
    }
  }

  // Call the insert hook on the element. Note that the insert hook is not supported on the module
  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
  }

  // Call module post hook
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
  return vnode;
}

function emptyNodeAt(elm: Element) {
  const id = elm.id ? '#' + elm.id : '';
  const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
  return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}

// The key is the same as the selector
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

patchVnode

The main logic is as follows:
  • Call the prepatch hook in the module (life cycle related)
  • Make differentiation according to the text and children differences between the old and new vnodes
  • It should be noted that when children exist in both old and new nodes and are different, diff the children and call updateChildren
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
  let i: any, hook: any;
  // Call the prepatch hook lifecycle hook
  if (isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
    i(oldVnode, vnode);
  }
  //Set the dom Association for the new vnode
  const elm = (vnode.elm = oldVnode.elm as Node);
  let oldCh = oldVnode.children;
  let ch = vnode.children;
  if (oldVnode === vnode) return;
  if (vnode.data !== undefined) {
    // Call the update hook life cycle hook on the module
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    i = vnode.data.hook;
    // Call the update hook lifecycle hook on vnode
    if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
  }
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // diff the children when the old and new nodes have children and they are different
      if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
    } else if (isDef(ch)) {
      // The old node does not have children, and the new node has children
      // Old node exists and text is null
      if (isDef(oldVnode.text)) api.setTextContent(elm, '');
      // Add a new vnode
      addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // The new node does not have children. The old node has children. Remove the children of the old node
      removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
    } else if (isDef(oldVnode.text)) {
      // Old node exists and text is null
      api.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // Update text
    api.setTextContent(elm, vnode.text as string);
  }
  // Call the postpatch hook lifecycle hook
  if (isDef(hook) && isDef((i = hook.postpatch))) {
    i(oldVnode, vnode);
  }
}

Core method updateChildren


When both the new Vnode and the old Vnode have children, enter the method.

  • Assign newStartIdx oldStartIdx newEndIdx oldEndIdx to the old and new node s respectively for pointer loop traversal
  • Increase the number of nodes that do not conform to the same, and replace other patchvnodes
  • At the end of the cycle, process the remaining nodes for batch deletion or batch addition
  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, elmToMove, refElm
 
    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly
 
    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] : null
        if (isUndef(idxInOld)) { // New element
          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 {
            // same key but different element. treat as new element
            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)
    }
  }

Map summary

https://juejin.cn/post/6844903671906435080#heading-0
https://segmentfault.com/a/1190000017519084
https://www.cnblogs.com/lilicat/p/13448827.html
The source code comments of this article refer to articles such as nuggets Xunlei front end and segment fault-chen4342024

Topics: Vue diff