Vue2.0 and Vue3.0 Dom Diff comparison

Posted by PHPnewby! on Wed, 29 Dec 2021 07:28:58 +0100

In increasingly sophisticated front-end applications, state management is a frequently mentioned topic. From the early days of slash and burn to jQuery, and now to the popular era of MVVM, the form of state management has changed dramatically. Instead of updating the view by maintaining a wide range of event callbacks and monitoring, we use two-way data binding. By simply maintaining the corresponding data state, the view can be automatically updated, greatly improving development efficiency.

However, two-way data binding is not the only way, and there is a very rough and effective way to redraw the entire view, that is, to reset innerHTML, once the data changes. It's really simple, rough, and effective, but if you update the entire view just because a small piece of data changes, the cost-effectiveness ratio will be too low, and things like events, input boxes to get focus, and so on, will need to be reprocessed. Therefore, for small applications or local small views, this is entirely possible, but it is not advisable for large and complex applications.

To solve the problem that views can be updated reasonably and efficiently with a lot of frequent data updates, Vue introduced virtual DOM in version 2.0. Virtual DOM adds maintainability at the expense of some performance, implements centralized operation of DOM, modifies the virtual DOM when data changes, and then reflects it in the real DOM, updates the DOM at the lowest cost to improve efficiency; In addition, Virtual DOM can render on an end other than DOM, such as Weex.

Introduction to diff algorithm

The purpose of the diff algorithm is to find out which nodes have changed and which nodes have not changed so that they can be reused. If you use the most traditional diff algorithm, as shown in the figure below, each node traverses all the nodes in another tree to compare, that is, the complexity of o(n^2), plus the complexity of o(n) when updating the nodes, then the total complexity of o(n^3) is reached, which is very costly for a page with a large number of complex nodes in the structure.

In fact, both vue and react optimize the diff algorithm of virtual dom to reduce the complexity to o(n). The specific strategies are:

  1. Only nodes at the same level can compare with each other


Traverse the entire number of nodes from the root node, only comparing the nodes at the same level. So in our code development, if the content of the node does not change, it is not easy to change its hierarchy, otherwise the node can not be reused.

  1. When comparing nodes, if the types are different, destroy the new node and all its children directly.
  2. For children of the same type, use the key to help find and use the algorithm to optimize the search efficiency. The diff algorithms of react and vue2 and vue3 are different.

Next we'll look at the implementation of the specific code in vue

Diiff algorithm in Vue2

Overall flowchart

patch

First decide if it's the first rendering, then we'll just create teElm if it's the first. If you don't go to determine if the element types of the old and new nodes are the same; If both nodes are the same, examine their children in depth. If the two nodes are different, then the Vnode has been completely changed, and the oldVnode can be replaced directly.

function patch(oldVnode, vnode, hydrating, removeOnly) {
    // Determine if the new vnode is empty
    if (isUndef(vnode)) {
      // If the old vnode does not uninstall all the old vnodes for empty
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    // Used to store insert hook functions, called before inserting a node
    const insertedVnodeQueue = []
    // If the old node does not exist, create the new node directly
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // Is it an element node
      const isRealElement = isDef(oldVnode.nodeType)
      // patchVnode updates occur when the old node is not a real DOM node and the new and old nodes have the same type and key
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // If not the same element node
        // When the old node is a real DOM node
        if (isRealElement) {
          // If it is an element node and modify SSR_in the SSR environment ATTR Properties
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            // Is rendered by the server, delete this property
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          // This judgement is the processing logic of server-side rendering
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            }
          }
          // If it is not rendered on the server side or if mixing fails, create an empty comment node to replace the oldVnode
          oldVnode = emptyNodeAt(oldVnode)
        }

        // Get the parent node of oldVnode
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // Create a DOM node from the new vnode and mount it on the parent node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // If the root node of the new vnode exists, that is, the root node has been modified, the update parent node needs to be traversed
        // Recursively update parent placeholder elements
        // Is the hook function that executes destory and create and insert of the parent node once
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          // Update placeholder elements for parent components
          while (ancestor) {
            // Uninstall all components under the old root node
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            // Replace existing elements
            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)
            }
            // Update parent node
            ancestor = ancestor.parent
          }
        }
        // Delete old nodes if they still exist
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          // Otherwise, uninstall oldVnode directly
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // insert hook function to execute virtual dom
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    // Returns the elm of the latest vnode, which is the real dom node
    return vnode.elm
 }

patchVnode

  • If Vnode and oldVnode point to the same object, return directly;
  • Assigning the true DOM of the old node to the new node (connecting the real DOM to the new child node) is called elm, then iterating through all the attributes on the oldVnode, such as class,style,attrs,domProps,events, etc., by calling update;
  • If both old and new nodes have text nodes and the text is different, then use vnode.text updates the text content.
  • If oldVnode has children and Vnode does not, delete the old node directly.
  • If oldVnode has no child nodes and Vnode has, then the child nodes of Vnode can be added to the DOM after they are authenticated.
  • If both have child nodes, the updateChildren function is executed to compare the child nodes.
 function patchVnode(
    oldVnode, // Old Virtual DOM Node
    vnode, // New Node
    insertedVnodeQueue, // Insert Node Queue
    ownerArray, // Array of nodes
    index, // Subscript of current node
    removeOnly
  ) {
    // New and old nodes compare addresses, skip directly
    if (oldVnode === vnode) {
      return
    }
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    const elm = vnode.elm = oldVnode.elm
    // Skip checking for asynchronous components if the current node is commented or v-if or an asynchronous function
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // When the current node is a static node, the key is the same, or when there is a v-once, the direct assignment returns
    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.prepatch)) {
      i(oldVnode, vnode)
    }
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      // Traverse calls update to update all properties of oldVnode, such as class,style,attrs,domProps,events...
      // Here the update hook function is the hook function of vnode itself
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // The update hook function here is the function we passed in
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // If the new node is not a text node, that is, it has children
    if (isUndef(vnode.text)) {
      // If both old and new nodes have children
      if (isDef(oldCh) && isDef(ch)) {
        // If the children of the old and new nodes are different, the updateChildren function is executed to compare the children
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // If the new node has children, the old node has no children

        // If the old node is a text node, that is, no child node, empty
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // Add a new node
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // If the new node has no children and the old node has children, delete
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // If the old node is a text node, empty it
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // Update the text if the text of the old node is different from the text of the new node
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
 }

updateChildren

Here are the implementation details of the diff algorithm, including the following processes:

  1. Condition one: Compare the header nodes of newChildren with oldChildren. See if both nodes have the same type and key. If the same, update the node (patchVnode); then move the two header pointers backwards (that is, oldStartIdx++ && newStartIdx++), and the two headers oldStartVnode and newStartVnode revalue; the current loop ends and enters the next cycle, followed by the comparison of the new and old headers until the two node types and keys are different, entering the second condition judgment.
  2. Condition two: tail node comparison. If the two nodes are the same, do the patchVnode work. Then two tail pointers move forward (that is, oldEndIdx-- && newEndIdx--), two tail oldEndVnode s and newEndVnode s revalue; the current loop ends and enters the next cycle. Then the first condition determines that condition one is not valid; the second condition determines that condition two-tail node comparison is still not valid; the third condition determines that condition three is valid.
  3. Condition three: newChildren's last unhandled child node is compared with oldChildren's first unhandled child node. If the nodes are the same, work on the patchVnode, and the real DOM of oldStartVnode moves to the corresponding location of the new EndVnode. Then the old head pointer moves back, the new tail pointer moves forward (that is, oldStartIdx+ && newEndIdx--), oldStartVnode and newEndVnode revalue; the current loop ends and enters the next cycle. Continue to compare, the new and old head pointers are different, the tail pointers are different, the two heads and tails are different, and enter the judgment of condition four.
  4. Condition four: The first child node that newChildren did not process is compared with the last child node that oldChildren did not process. If the nodes are the same, work on the patchVnode, and the real DOM of the oldEndVnode moves to the old oldStartVnode. Front of elm; The old tail pointer moves forward, the new head pointer moves backward (that is, oldEndIdx--&& newStartIdx+), oldEndVnode and newStartVnode revalue. Enter the next cycle, if none of the above conditions is true, enter the judgment of condition five.
  5. Condition 5: The above four conditions are not satisfied. There are two main cases. First, if there is a key in both the old and new child nodes, a hash table will be generated based on the key of the unprocessed oldChildren. Matching the hash table with the key of newStartVnode will determine if the newStartVnode and the matching node are sameVnode. If so, make an update of the two same nodes, and set the node in the matched oldCh to null. Then move the corresponding node in the real DOM to the front, otherwise, insert the corresponding node generated by the newStartVnode into the corresponding position in the dom. If there is no key, the newStartVnode generates a new node and inserts it directly into the real DOM. The newStartVnode and newStartIdx pointers move back and make the next loop until oldStartIdx is greater than oldEndIdx or newStartIdx is greater than newEndIdx.

Finally, if the head pointer of the old node is larger than the tail pointer of the old node, it means that there are many new nodes, create the remaining new nodes and insert them into the DOM directly. Conversely, if the new node's head pointer is larger than the new node's tail pointer, the number of old nodes is large, delete the remaining old nodes.

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue) {
    let oldStartIdx = 0 // Subscripts traversed by old vnode
    let newStartIdx = 0 // Subscripts traversed by new vnode
    let oldEndIdx = oldCh.length - 1 // Old vnode list length
    let oldStartVnode = oldCh[0] // First child element of the old vnode list
    let oldEndVnode = oldCh[oldEndIdx] // Last child element of the old vnode list
    let newEndIdx = newCh.length - 1 // New vnode list length
    let newStartVnode = newCh[0] // The first child element of the new vnode list
    let newEndVnode = newCh[newEndIdx] // Last child element of the new vnode list
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
    // Loop, the rule is to start the pointer moving to the right and end the pointer moving to the left
    // End the loop when the start and end pointers coincide
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
        
        // Old Start vs New Start
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // Is the same node recursive call to continue comparing the contents and child nodes of the two nodes
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // Then move the pointer back one place, and compare from there to there
        // For example, compare [0] for the first time between two lists, and then compare [1]... The same is true after
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        
        // Old End vs New End
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // Then move the pointer forward by one place, from back to front
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        
        // Old Start vs New End
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // The old list values from go-back, the new list values from go-back to go-forward, and then compare
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        
        // Old End vs New Start
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // The old list values from the back to the front, the new list values from the front to the back, and then the comparison
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
        
        // None of the above four situations hit
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // Get the new start key and look in the old children to see if there's a node with this key
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          
        // There is a new child ren, but no corresponding element is found in the old child ren
        if (isUndef(idxInOld)) {
          ///Create a new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // Found the corresponding element in the old children
          vnodeToMove = oldCh[idxInOld]
          // Determine if labels are the same
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // Make an update of two identical nodes
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
           	nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // If the labels are different, create a new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // OldStartIdx > oldEndIdx indicates that the old vnode has finished traversing first
    if (oldStartIdx > oldEndIdx) {
      // Add a node from newStartIdx to newEndIdx
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    
    // Otherwise, the new vnode traverses first
    } else if (newStartIdx > newEndIdx) {
      // Delete nodes that are not traversed in the old vnode
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }
  
  function isUndef (v) { return v === undefined || v === null }

Diff algorithm for Vue3

Without key

Step 1: Compare the length of old and new children s to get the minimum value, and then patch the public part again.

Step 2: Determine the number of new and old nodes. If the number of old nodes is greater than the number of new nodes,'unmountChildren'will be all old nodes directly after the public part. If the number of new nodes is greater than the number of old nodes, then all new nodes directly after the public portion of'mountChildren'.

const patchUnKeyedChildren = (n1,n2,container,anchor) => {
	const oldLen = n1.length;
  const newLen = n2.length;
  const commonLen = Math.min(oldLen, newLen);
  for(let i = 0;i < commonLen; i++){
  	const nextChild = n2[i]
    patch(c1[i], nextChild, container, null)
  }
  if(oldLen > newLen){
  	unmountChildren(c1,commonLen)
  }else{
  	mountChildren(c2,container,anchor,commonLen)
  }
}

With key

c1: a b [ c d e ] f g
c2: a b [ d e c h ] f g
If c1 and c2 above are new and old children, there will be a preprocessing process at diff time.
Compare from go to go first, and when nodes are different, do not compare from go back. The comparison is then made from the back to the front, and when the nodes are different, the comparison is no longer made forward.
After preprocessing, the parts that c1 and c2 really need to diff are as follows:
c1: c d e
c2: d e c h
Finally, the "longest increasing subsequence" is used to complete the comparison of the above differences and improve diff efficiency.

Contrast from the beginning

First define three variables i = 0 (representing the comparison starting at the node in the following table); E1 = c1.length-1 (representing the last node in the old node sequence in the following table); E12 = c2.length-1 (representing the last node in the new node sequence in the following table).

In the while loop, the patch works when the nodes are the same, starting from the head of the old and new node sequence. Exit the current loop when a different node is encountered.

const isSameVNodeType = (n1:VNode, n2:VNode) => n1.type === n2.type && n1.key === n2.key;
// 1. sync from start
while(i<=e1 && i<=e2){
	let n1 = c1[i];
  let n2 = c2[i];
	if(isSameVNodeType(n1, n2)){
  	patch(n1,n2,container);
  }else{
  	break;
  }
  i++
}

Compare from tail

In the same way, we do patch work in a while loop, starting with the tail nodes of the old and new node sequences. Exit the current loop when a different node is encountered.

// 2. sync from end
while(i<=e1 && i<=e2){
  let n1 = c1[el];
  let n2 = c2[e2];
  if(isSameVNodeType(n1, n2)){
  	patch(n1,n2,container);
  }else{
  	break;
  }
	e1--;
  e2--;
}

Same Sequence + Mount

After the above two loops, it is found that the new node sequence is more than the old one. When this happens, we can simply cycle through and mount all the extra nodes to their corresponding locations. Attention needs to be paid to whether to mount forward or backward. Here we use the current E2 value plus 1 to judge, if e2+1>e2 then insert backwards and vice versa insert forward

// 3. common sequence + mount
if(i>e1){
	if(i<= e2){
  	const nextPos = e2 + 1;
   	const anchor = nextPos < l2 ? c2[nextPos].el : null;
    while(i<=e2){
    	patch(null,c2[i],container,anchor);
      i++;
    }
  }
}

Same sequence + uninstall

Same as above, when the two loops end/end, it is found that the old node sequence is more than the new one. Then we simply loop through the unnecessary old sequence nodes

// 4. common sequence + unmount
if(i>e2){
	while(i <= e1){
  	 unmount(c1[i]);
     i++;
  }
}

Unknown sequence comparison

Sometimes, the situation may be less simple than the above, that is, when i, e1, e2 do not satisfy the above situations, we want to find out which nodes need to be removed, added, or determine which ones need to be moved.

To do this, we need to traverse the unhandled nodes in c1 and see if there are any corresponding nodes in c2 (with the same key). If not, it means that the node has been removed, then perform the unmount operation.

First, in order to quickly confirm whether the nodes of c1 have corresponding nodes in C2 and where they are located, a mapping table (key: index) is established for the nodes in c2.

Then, based on the number of nodes to be processed (toBePatched) in c2, a variable, newIndexToOldIndexMap (Map <newIndex, oldIndex>), is created to store the index correspondence between the old and new node sequences.

Then, traverse the nodes to be processed in c1; First, determine if patched is greater than the number of nodes to be processed in C2 toBePatched. When patched > toBePatched, you can assume that the remaining nodes in C1 are redundant, so it is better to remove them directly. Then it determines whether there are any nodes with the same key in the remaining C1 and c2.

  • No, unmount the node in the current c1 directly
  • Yes, patch work; Update the value of the corresponding position in the newIndexToOldIndexMap to the index of the node in c1; Then determine if the element needs to be moved; Finally, update the patched;

Finally, after finding the longest incremental subsequence based on the longest incremental subsequence algorithm, traverse the nodes to be processed in c2 to determine whether the nodes are new or need to be moved.

// 5. unknown sequence
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
for (i = s2; i <= e2; i++) {
    const nextChild = c2
    if (nextChild.key != null) {
        keyToNewIndexMap.set(nextChild.key, i)
    }
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j
let patched = 0
const toBePatched = e2 - s2 + 1 // Number of nodes to process in c2
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0 // Maximum index value for traversed c1 nodes to be processed in c2

// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
const newIndexToOldIndexMap = new Array(toBePatched) // Used to find the longest incremental subsequence later
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]
    if (patched >= toBePatched) {
        // all new children have been patched so this can only be a removal
        unmount(prevChild, parentComponent, parentSuspense, true)
        continue
    }
    let newIndex
    if (prevChild.key != null) {
        newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
        // key-less node, try to locate a key-less node of the same type
        for (j = s2; j <= e2; j++) {
            if (
                newIndexToOldIndexMap[j - s2] === 0 &&
                isSameVNodeType(prevChild, c2[j])
            ) {
                newIndex = j
                break
            }
        }
    }
    if (newIndex === undefined) {
        unmount(prevChild, parentComponent, parentSuspense, true)
    } else {
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
        } else {
            moved = true
        }
        patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
        )
        patched++
    }
}

// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex] as VNode
    const anchor =
        nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
    if (newIndexToOldIndexMap[i] === 0) {
        // mount new
        patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
        )
    } else if (moved) {
        // move if:
        // There is no stable subsequence (e.g. a reverse)
        // OR current node is not among the stable sequence
        if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
        } else {
            j--
        }
    }
}

The diff of Vue3 compared with the optimized part of Vue2

  • Vue2 is a full Diff (when the data changes, it generates a new DOM tree and compares it with the previous DOM tree, finds different nodes and updates it); Vue3 is a static tag + a partial Diff (When Vue 3 creates a virtual DOM tree, it adds a static tag based on whether the contents of the DOM will change. Then, when compared with the last virtual node, only those nodes with static tags will be compared.)
  • Optimizing the comparison process with the longest incremental subsequence minimizes DOM movement and minimizes DOM operations

Role of Key attribute s

When Vue is updating the list of elements rendered with v-for, it defaults to the in-place update policy. If the order of the data items is changed, Vue will not move the DOM elements to match the order of the data items, but will update each element in place and ensure that they are rendered correctly at each index location. This looks like Vue 1. Track-by='$index'for X. Although this default mode is efficient, it only applies to list rendering output that does not depend on subcomponent state or temporary DOM state (for example, form input values).

In order to give Vue a hint that it can track each node to reuse or reorder existing elements, we need to provide a unique identification key attribute for each item when v-for renders the list of elements.

Use scenarios for Key and how to use them correctly

Use scenarios:

    • When v-for renders a list of elements
    • When a child element has the same parent element
    • You can also set a key attribute when you want to force the replacement of an element/component instead of reusing it. For example, trigger a transition
// When the text changes, <span>is always replaced, not modified, and therefore triggers a transition.
<transition>
  <span :key="text">{{ text }}</span>
</transitio
    • When traversing the DOM content of the output is very simple or when we deliberately rely on default behavior for performance improvements, you can set a key attribute instead.

Wrong usage:

  1. Use index or index to stitch together other values as key s
  2. Use non-basic type values such as objects or arrays as key s for v-for

Correct usage:

  1. Use string or numeric value as key value
  2. Use unique values as key s, for example, id, etc.

Reference material:

https://stackoverflow.com/questions/44238139/why-is-vue-js-using-a-vdom
Announcing Vue.js 2.0 - Know
https://cn.vuejs.org/v2/guide/list.html
https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/renderer.ts

This article is from internal sharing!

Topics: Javascript Front-end Vue.js