The DIFF algorithm adopted by Vue 3.0 is slightly different from the double-ended comparison of 2.0. The general principle is as follows

// c1: a b [ c d e ] f g  
// c2: a b [ d e c h ] f g
Copy the code

If c1 and C2 are old and new children, there will be a pre-processing process during diff.

Comparisons are made from the front to the back, and when the nodes are different, no further comparisons are made. Then it compares from the back to the front, and when the nodes are different, it does not compare from the front.

After pretreatment, the parts that really need to be diff for C1 and C2 are as follows:

// c1: c d e
// c2: d e c h
Copy the code

Finally, the “longest increasing subsequence” is used to complete the comparison of the above differences and improve the efficiency of DIFF.

Process the same front and back nodes

The preprocessing code is as follows:

const patchKeyedChildren = (
    c1,
    c2,
    container,
    parentAnchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
) => {
    let i = 0;  
    const l2 = c2.length
    let e1 = c1.length - 1
    let e2 = c2.length - 1

    // 1. sync from start
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = c2[i]
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      i++
    }

    // 2. sync from end
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = c2[e2]
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }
}

Copy the code

Only nodes are added or removed

Another benefit of preprocessing is that in some cases, we can know exactly when a node is being added or removed.

  • New nodes

If I, E1, and E2 satisfy the following relationship, nodes are added

// 3. common sequence + 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) {
    if (i <= e2) {
        const nextPos = e2 + 1;
        const anchor = nextPos < l2 ? c2[nextPos] : parentAnchor

        while (i <= e2) {
            patch(
                null,
                c2[i],
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG
            )
            i++
        }
    }
} else if {
    //
} else {
    //
}
Copy the code
  • Node to remove

If I, E1, and E2 satisfy the following relationships, nodes are removed

// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
if (i > e1) {
  //
} else if (i > e2) {
    while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
    }
} else {
    //
}
Copy the code

A node has been moved, added, or deleted

Sometimes the situation may not be as simple as the above, that is, I, E1, E2 do not satisfy the above two cases, we need to find the nodes to be removed, new nodes, or determine which nodes need to be moved.

To do this, we need to go through the unprocessed nodes in C1 and see if there are corresponding nodes (with the same key) in C2. If no, the node is removed. In this case, unmount the node.

First, in order to quickly confirm whether the node of C1 has a corresponding node in C2 and its location, a mapping (key: index) is established for the nodes in C2.

// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [d e c h] f g
// i = 2, e1 = 4, e2 = 5
if (i > e1) {
  //
} else if (i > e2) {
  //
} else {
    const s1 = i
    const s2 = i

    const keyToNewIndexMap = new Map(a)// 5.1 build key:index map for newChildren
    for (i = s2; i <= e2; i++) {
        const nextChild = c2[i]
        if(nextChild.key ! = =null) {
            keyToNewIndexMap.set(nextChild.key, i)
        }
    }
}

Copy the code

Next, define the following variables

let j 
let patched = 0
const toBePatched = e2 - s2 + 1  // Number of nodes to be processed in c2
let moved = false

// used to track whether any node has moved
let maxNewIndexSoFar = 0  // The maximum index in C2 for the traversed c1 node to be processed

// 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 the longest increasing subsequence

for (i = 0; i < toBePatched; i++) {
    newIndexToOldIndexMap[i] = 0
}
Copy the code

Then, the nodes to be processed in C1 are traversed to determine whether a node with the same key exists in C2.

  • If no, the node is removed. Unmount the node.
  • If yes, call patch. And records the index of the node in C1. At the same time, the maximum index of the node in C2 is recorded. If the index position of the node in C2 is less than the maximum index, it indicates that some elements need to be moved.
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
for (i = s1; i <= e1; i++) {
    const prevChild = c1[i]

    // (A)

    let newIndex 
    if(prevChild.key ! = =null) {
        newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
        for (j = s2; i <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j])
            ) {
              newIndex = j
              break}}}if (newIndex === void 0) {
        unmount(prevChild, parentComponent, parentSuspense, true)}else {
        newIndexToOldIndexMap[newIndex  - s2] = i + 1  // (B)

        if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
        } else {
            moved = true
        }
        patch(
            prevChild,
            c2[i],
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
        )

        patched++  // (C)}}Copy the code

Do all nodes in C1 need to search for corresponding nodes in C2 and then call patch?

Note in code (C) above that we update the number of nodes we have patched, so when patched > toBePatched, consider the nodes in c1 to be redundant and remove them.

So we need to add some code at (A) above

if (patched >= toBePatched) {
    // all new children have been patched so this can only be a removal
    unmount(prevChild, parentComponent, parentSuspense, true)
    continue
}
Copy the code

This is where it gets harder to understand.

At the beginning, we said that after preprocessing, the remaining nodes will use the longest increasing subsequence to improve diff efficiency.

The main purpose of solving the longest increasing subsequence is to reduce the movement of DOM elements, which can also be understood as the least DOM operation.

First, we need to solve for the longest increasing subsequence

// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR 
Copy the code

Let’s see what the value of newIndexToOldIndexMap is here.

For a specific example, let’s say C1 and C2 are shown below

Defines and initializes the newIndexToOldIndexMap

const newIndexToOldIndexMap = new Array(toBePatched)

for (i = 0; i < toBePatched; i++) {
    newIndexToOldIndexMap[i] = 0
}
Copy the code

ToBePatched indicates the number of nodes to be processed in C2 after preprocessing. So in this case, there’s going to be

toBePatched = 4
newIndexToOldIndexMap = [0.0.0.0]
Copy the code

Note that at (B) of the code in 5.2 traversing the nodes in C1 above, there is

// This is I + 1, not I
// Because 0 is a special value, it means this is a new node
newIndexToOldIndexMap[newIndex  - s2] = i + 1  // (B)
Copy the code

So after processing the nodes in C1, we’re going to have

moved = true
newIndexToOldIndexMap = [4.5.3.0]
Copy the code

So, increasingNewIndexSequence value is getSequence (newIndexToOldIndexMap) return values

// [4, 5, 3, 0] --> the longest increasing subsequence is [4, 5]
// The corresponding index is [0, 1]
increasingNewIndexSequence = [0.1]
Copy the code

After the longest increasing subsequence is solved, what is left is to traverse the nodes to be processed in C2 to determine whether the nodes are added or not and whether they need to be moved.

j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
// Note: this is traversing from back to front
for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex]
    const anchor =
        nextIndex + 1 < l2 ? (c2[nextIndex + 1]).el : parentAnchor

    The value in newIndexToOldIndexMap is initialized to 0 by default
    // Where === 0 indicates that the node in c2 has no corresponding node in C1 and is added
    if (newIndexToOldIndexMap[i] === 0) {
        // mount new
        patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
        )
    } else if (moved) {
        // move if:
        // There is no stable subsequence (e.g. a reverse)
        // OR current node is not among the stable sequence
        
        // j < 0 --> []
        if (j < 0|| i ! == increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) }else {
            j--
        }
    }
}
Copy the code

Longest increasing subsequence

In computer science, the longest increasing subsequence problem refers to finding a subsequence in a given numerical sequence, making the number of elements in the subsequence increase successively, and the length of the subsequence as large as possible. The elements in the longest increasing subsequence are not necessarily continuous in the original sequence. “– Wikipedia

For the following original sequence 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15, the longest increasing subsequence is 0, 2, 6, 9, 11, 15. It is worth noting that the longest increasing subsequence of the original sequence is not necessarily unique. For the original sequence, there are actually two longest increasing subsequences 0, 4, 6, 9, 11, 15 0, 4, 6, 9, 13, 15Copy the code

The last

At this point, Vue 3.0 diFF code is analyzed, welcome to discuss.

The specific code: renderer.ts

Vue 3.0 DIFF algorithm and its principle