Analysis of diff algorithm in vue3

At present, most of the analysis of Vue3 is about double-pointer comparison and the generation of the longest incremental subsequence, but the combination with the source code is relatively small. This article tries to do some analysis combined with the source code to help you deepen your understanding of the vue diff algorithm. At the same time, other articles seem to have less conceptual analysis of Vue 3 “blocks”, and this article also makes some explanations. The most difficult part of the diff algorithm is case 5 (5. unknown sequence, see the code below), this article does not describe this algorithm in detail, but more about how Vue decides to call these algorithms Yes, if you want to know about this aspect, you can read other articles (Talk about the core diff algorithm of vue2 and vue3 – Nuggets (juejin.cn)), they explain this aspect in more detail.

Whether it is vue2 or vue3, the time complexity of the diff algorithm is O(n), because it flattens the component structure instead of traversing the tree-based structure (thanks to the compilation phase Template analysis, which is also not possible with react). The most difficult thing for the diff algorithm to deal with is the case where both old and new nodes are arrays.

The key rendering function is patchElement, and its location in the vue source code is: vuejs.core\packages\runtime-core\src\renderer.ts(github.com/vuejs/ core)… The other functions mentioned below are basically in this file.

Component updates (blocks) for stable structures

For optimization, for a stable structure (excluding v-if, v-for), Vue will call the patchBlockChildren function to compare the old and new VNodes one by one. (For what is a stable structure, you can see the description on the official website of vue: rendering mechanism | Vue.js (vuejs.org)). dynamicChildren, only components with a stable structure have this attribute, so if this attribute exists, vue will call the patchBlockChildren function for update processing.

if (dynamicChildren) {<!-- -->
      // The Block block is a stable structure, that is, it does not include v-if v-for, and can be compared element by element
      patchBlockChildren(
        /** */
      )
      /** */
    } else if (!optimized) {<!-- -->
      // full diff
      patchChildren(
       /** */
      )
    }

The patchBlockChildren function looks like this:

for (let i = 0; i < newChildren.length; i ++ ) {<!-- -->
      const oldVNode = oldChildren[i]
      const newVNode = newChildren[i]
      // Determine the container (parent element) for the patch.
      const container = / ** xxx */ Here is to get the external container DOM of VNode,
      // If the new node is to be replaced directly, it needs to be mounted under it
      
      // decide how to update the dom
      patch(
        oldVNode,
        newVNode,
        container,
        null,
        parentComponent,
        parent Suspense,
        isSVG,
        slotScopeIds,
        true
      )
    }

Structure is not a stable way to update components

Then the next step is how to deal with structurally unstable components, that is, if there is v-if or v-for, then the old and new child nodes may Inconsistent, there may be new child nodes, old nodes may be removed, of course, they may be consistent, and the knowledge content has changed. The most common case is that we use v-for to render arrays.

At this time, the algorithms such as double-pointer comparison and finding the longest increasing subsequence come into play. The patchChildren function internally judges whether the subcomponent has the key attribute, if there is a key attribute in the subcomponent (not all of them need to be included) , the patchKeyedChildren function will be called, otherwise the patchUnkeyedChildren function will be called (patchFlag is used here to determine whether a comparison is required, and for components defined by component templates, patchFlag is generated during the compilation phase , which marks the dynamic properties of the component, if it is 0, it does not contain dynamic properties, and there is no need to compare):

patchFlag indicates what content needs to be updated, and shapeFlag indicates the node type;

// c1 and c2 are sub-arrays, compare the sub-arrays
const c1 = n1 & & n1.children
const prevShapeFlag = n1 ? n1. shapeFlag : 0
const c2 = n2.children

const {<!-- --> patchFlag, shapeFlag } = n2

if (patchFlag > 0) {<!-- -->
       // Compare only if there is a patch tag
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {<!-- -->
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          /** */
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {<!-- -->
        // unkeyed
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          /** */
        )
        return
      }
    }

Write it down to appreciate the double pointer comparison and longest increasing subsequence algorithm (inside the patchKeyedChildren function) that everyone talks about:

let i = 0
const l2 = c2. length
let e1 = c1. length - 1 // prev ending index
let e2 = l2 - 1 // next ending index

// 1. sync from start
// (a b) c
// (a b) d e
// Call the path algorithm from the head, patch one by one for the old and new stages of the same type, until there is no match
while (i <= e1 & amp; & amp; i <= e2) {
   const n1 = c1[i]
   const n2 = (c2[i] = optimized
     ? cloneIfMounted(c2[i] as VNode)
     : normalizeVNode(c2[i]))
   if (isSameVNodeType(n1, n2)) {
     patch(
       n1,
       n2,
       container,
       /** */
     )
   } else {
     break
   }
   i + +
}

// 2. sync from end
// a (b c)
// d e (b c)
// Same as above, synchronous tail
while (i <= e1 & amp; & amp; i <= e2) {
    const n1 = c1[e1]
    const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
    if (isSameVNodeType(n1, n2)) {
        patch(
            n1,
            n2,
            container,
            /** */
        )
    } else {
        break
    }
    e1--
    e2--
}

// In the third case, mount a new node
// i is the inconsistent position of the head, e1 is the inconsistent subscript at the end of the old node array, and e2 is the inconsistent subscript at the end of the new node array
// 3. common sequence + mount
// Two matching situations, the new array has new nodes to mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
// i > e1 old node has reached the end
// i <= e2 and new nodes exist, add new nodes
    if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
            patch(
                null, // no old node exists
                (c2[i] = optimized
                    ? cloneIfMounted(c2[i] as VNode)
                    : normalizeVNode(c2[i])),
                container,
                /** */
            )
            i + +
        }
    }
}

// 4. common sequence + unmount
// In the fourth case, uninstall the old node
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
// The new node has been traversed, there are still old nodes left, uninstall the old node
    while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i + +
    }
}

/** After that, the situation is more complicated. The old child nodes and the new child nodes have not been traversed.
* Consider how best to reuse old nodes
*/
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
    const s1 = i // prev starting index
    const s2 = i // next starting index

    // 5.1 build key: index map for newChildren
    // space for time, resume index table
    const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
    for (i = s2; i <= e2; i ++ ) {
        const nextChild = (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i]))
        if (nextChild. key != null) {
            if (__DEV__ & amp; & amp; keyToNewIndexMap.has(nextChild.key)) {
                warn(
                    `Duplicate keys found during update:`,
                    JSON. stringify(nextChild. key),
                    `Make sure keys are unique.`
                )
            }
            keyToNewIndexMap.set(nextChild.key, i)
        }
    }

    // 5.2 loop through old children left to be patched and try to patch
    // matching nodes & amp; remove nodes that are no longer present
    // Match old and new nodes and unload old nodes that don't exist
    let j
    let patched = 0
    const toBePatched = e2 - s2 + 1
    let moved = false
    // used to track whether any node has moved
    let maxNewIndexSoFar = 0
    // 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)
    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 & amp; & amp;
                    isSameVNodeType(prevChild, c2[j] as VNode)
                ) {
                    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,
               /** */
            )
            patched + +
        }
    }

    //5.3 move and mount
    // generate longest stable subsequence only when nodes have moved
    // Generate the longest increasing subsequence (let the least elements need to be 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,
                parent Suspense,
                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--
            }
        }
    }
}

Follow-up processing

Back to the patchElement function, we have already called patchChildren or patchBlockChildren to update the stable or unstable sub-array. But what we have done is update the children, so what about the component itself? So after updating the child nodes, go back to the component itself, update props, css, perform side effects, etc.

This is also consistent with the execution process of the vue component update – the parent node first triggers the update -> then the child node triggers the update -> the child node completes the update -> the parent node completes the update.

When the child node is updated, the responsive parameters such as the props of the parent node have changed (it is the update caused by the change of the parent node’s responsive data). If the parent node completes the update first, the child node may affect the state of the parent node. Therefore, the child node needs to complete the update, and finally the update is completed on the parent node (that is, the dom content update).