Talk to the interviewer about Diff__Vue3

Posted by flashback on Sun, 30 Jan 2022 16:12:48 +0100

This is the third article about diff, talking about vue3's diff ideas Ideas mainly come from vue-design project

[CSDN]Talk to the interviewer about Diff___React
[CSDN]Talk to the interviewer about Diff___vue2
[CSDN] talk to the interviewer about diff___ Vue3 (this article)

For a better reading experience, it is suggested to start from the first article

I am a front-end pupil. Some design principles are misunderstood in the text. You are welcome to discuss and correct them 😁😁😁, Thank you! Of course, there are good suggestions. Thank you for putting forward them

(joke)

Let's start

Vue3_diff

process analysis

This paper focuses on the patch process, and the specific details and boundaries are not considered.

Also note

  • In the explanation of diff in the three articles, in order to facilitate the display of node reuse, children are used to save the content. In fact, this is unreasonable, because different children also have recursive patch es
  • diff is not the whole of vue optimize, but only a part of it. For example, determine the node type during compile, and different mount/patch processing methods for different types.
    Vue2. diff of X is better than react, avoiding unnecessary comparison.

Let me first assume that there are the following nodes. Key is the key of Vnode and children represents the content of this node

// Previous node
const preNodes = [
  {key: "k-1", children: "<span>old1</span>"},
  {key: "k-2", children: "<span>old2</span>"},
  {key: "k-3", children: "<span>old3</span>"},
  {key: "k-4", children: "<span>old4</span>"},
  {key: "k-5", children: "<span>old5</span>"},
  {key: "k-6", children: "<span>old6</span>"},
]
// New node, last updated result
const nextNodes = [
  {key: "k-11", children: "<span>11</span>"},
  {key: "k-0", children: "<span>0</span>"},
  {key: "k-5", children: "<span>5</span>"},
  {key: "k-13", children: "<span>13</span>"},
  {key: "k-1", children: "<span>1</span>"},
  {key: "k-7", children: "<span>7</span>"},
  {key: "k-16", children: "<span>16</span>"},
  {key: "k-3", children: "<span>3</span>"},
  {key: "k-15", children: "<span>15</span>"},
  {key: "k-17", children: "<span>7</span>"},
  {key: "k-4", children: "<span>4</span>"},
  {key: "k-6", children: "<span>6</span>"}
]

diff is based on the old and new diffs. We should first clarify this premise. If there are no nodes at the beginning, we will mount instead of patch.
Finally, the expected result (the old nodes are reused)

In addition, new nodes are based on old nodes, so

// The final node data, because the final node is based on the old node, here is a simulation
let newNodes = JSON.parse(JSON.stringify(preNodes));

preNodes: old nodes;
nextNodes: new node;
newNodes: a newly generated node, which is finally used to render as a real dom In fact, vue2 early stage is to generate new nodes completely first, and then render them as real dom In later versions, the corresponding DOM will be updated when the optimization traversal is changed to a patch

Refining the central idea: find the same key node from the old node and reuse it.

Detailed ideas:

1. First find the nodes with the same key s at both ends, and then find them in the middle.

As shown in the figure, J looks from the beginning. preEndIndex and nextEndIndex correspond to the end indexes of the old node and the new node respectively. If found, increase J or decrease preEndIndex and nextEndIndex.

The code is as follows

let j = 0;

let preEndIndex = preNodes.length - 1;
let nextEndIndex = nextNodes.length - 1;
let preVNode = preNodes[j];
let nextVNode = nextNodes[j];

while(preVNode.key === nextVNode.key){
  j++;
  preVNode = preNodes[j];
  nextVNode = nextNodes[j];
}
preVNode = preNodes[preEndIndex];
nextVNode = nextNodes[nextEndIndex];
while(preVNode.key === nextVNode.key){
  preVNode = preNodes[--preEndIndex];
  nextVNode = nextNodes[--nextEndIndex];
}

Considering a situation

In the above case, if the old node comparison is completed and the new node still exists, it will eventually cause J > prendindex [case 1]. Similarly, if the old node comparison is not completed and the new node comparison has been completed, then J > nextendidnex [case 2] will appear
In both cases,

  • Avoid loops and introduce label to solve them.
  • In addition, case 1 needs to delete redundant nodes (from newNodes), and case 2 needs to add nodes that are not traversed in the new nodes (to newNodes).

So change the original code:

// ....
outer: {
  while(preVNode.key === nextVNode.key){
    j++;
    if(j> preEndIndex || j > newEndIndex) {
      break outer;
    }
    preVNode = preNodes[j];
    nextVNode = nextNodes[j];
  }
  preVNode = preNodes[preEndIndex];
  nextVNode = nextNodes[nextEndIndex];
  while(preVNode.key === nextVNode.key){
    if(j> preEndIndex || j > nextEndIndex) {
      break outer;
    }
    preVNode = preNodes[--preEndIndex];
    nextVNode = nextNodes[--nextEndIndex];
  }
}
if(j > preEndIndex) {
  // After traversing the old node, the new node still exists. Put the new node into.
  for(let i = j; i< nextEndIndex; i++){
    const addedNode = nextNodes[i];
    // Note: within the framework, the dom is updated with appendchild
    newNodes.splice(i,0,addedNode);
  }
}
else if(j > nextEndIndex){
  // After traversing the new node, the old node still exists. Delete the old node.
  const deleteLen = preEndIndex - j;
  // Note: inside the framework is to rewrite removeChild to update dom
  newNodes.splice(i,deleteLen);
}else {  //When there are different nodes}

In most cases, it is just like the initial example: there are still different nodes in the middle, which need to be moved and added. This situation is the main processing area of patch, and the code is written in else above.

What is the specific idea,

2. Generate a mapping array of old nodes and reusable nodes

Mr. into an array noPatchedIndex with each item of - 1, and the length is the length of the node that the new node has not traversed.

Traverse the old and new nodes that are not compared (here is an optimization detail: the new node does not need to traverse. Because of the particularity of the structure, the object keyInIndex corresponding to the index of the key is directly generated, such as {k-1:0, k-2:1,...}, Take it directly from the back).

In the unmatched old node, if there is a node that is the same as the unmatched new node, save its index in the old node in the corresponding position of noPatchedIndex.

In addition, when judging that the new node does not exist, the old node needs to be deleted, and whether the element needs to be moved is obtained (there is a non incremental sort in the index array noPatchedIndex, that is, the current item in the array cannot be greater than the subsequent item)

About that

Newly generated node. k-2 does not exist in the new nodes, so it is deleted.

3. Handle the situation that needs to be moved (multiplexing node processing)

The steps are as follows:

  • ① Find the largest increasing subsequence lisArr index array of noPatchedIndex
  • ② Insert the unmatched new node and into the new node in turn.

For ①, there is a function lis,

lis([3,1,5,4,2]) //[1,4] | 1,2 is the largest increasing subsequence
lis([1,2,3]) //[0, 1, 2] | 1,2,3 is the largest increasing subsequence
lis([0,-1,8,6,10,7]) //[1,3,5] - 1,6,7 are the largest increasing subsequences

For the specific implementation of lis, please refer to my other article Algorithm chapter - finding the maximum increasing subsequence

So there

For ②, why find the largest increasing subsequence lisArr? Because the order of items in lisArr does not need to be moved, and the unmatched nodes of new nodes only need to be inserted before and after these items.
The specific implementation is to traverse noPatchedIndex and lisArr:

  1. The noPatchedIndex item is equal to - 1, which means that items that do not exist in the old node need to be added
  2. When the noPatchedIndex index is not equal to lisArr, you need to move the old node to the response location
  3. When noPatchedIndex index is equal to lisArr, no operation is performed.

Note: traversal is from back to front. The purpose is to prevent the array length transformation from affecting the index value and then the node value, insertion and deletion.

For intuitive understanding, here are a wave of operation diagrams: i and j are noPatchedIndex and lisArr

Deleted nodes are red, newly added nodes are green, and reused nodes are gray;
When a node is inserted, it calls the DOM API insertBefore This node is added back first. If there is duplication, the original node will be deleted;
The insertion position is the position of the node nextNodes[nopatchedIndex[lisArr[j]]] corresponding to the oldNodes

1)i =10, j = 2; Node k-4 multiplexing

2)i=9, j=1

3)i=8, j=1. newly added

4)i=7,j=1. k-3 multiplexing

5)i=6, j=0

6)i=5, j=0

7)i=4, j=0. Insertion (involving addition and deletion). Here, because the old node is inserted, and the original node (2) is after the insertion position (0), the index position deleted after adding should be reduced by one (it will be reflected in the code)

8)i=3,j=0, new

9)i=2, j=0. Insertion (involving addition and deletion). Here, because the old node is inserted, and the original node (9) is after the insertion position (0), the index position deleted after adding should be reduced by one (it will be reflected in the code)

10)i=1,j=0

11)i=0, j=0, new

12) i= -1, the cycle ends.

Finally, the results show that K-5, k-1, k-2, K-4 and K-6 have been reused, so where is k-2? The new node nextNodes does not contain this node, which is naturally deleted before moving! Mentioned earlier.
So far, the diff process is over. I believe you can see it by looking at the picture

Hey, drawing is too tired 🤣, Now I'm really sorry for the blogger risby who has a picture explanation for the article 🙏🙏🙏 (respect!!!). absolute!!!

Finally, I will send you all the codes.

All codes

// Old node
const preNodes = [
  {key: "k-1", children: "<span>old1</span>"},
  {key: "k-2", children: "<span>old2</span>"},
  {key: "k-3", children: "<span>old3</span>"},
  {key: "k-4", children: "<span>old4</span>"},
  {key: "k-5", children: "<span>old5</span>"},
  {key: "k-6", children: "<span>old6</span>"},
]
// New node
const nextNodes = [
  {key: "k-11", children: "<span>11</span>"},
  {key: "k-0", children: "<span>0</span>"},
  {key: "k-5", children: "<span>5</span>"},
  {key: "k-13", children: "<span>13</span>"},
  {key: "k-1", children: "<span>1</span>"},
  {key: "k-7", children: "<span>7</span>"},
  {key: "k-6", children: "<span>6</span>"},
  {key: "k-3", children: "<span>3</span>"},
  {key: "k-15", children: "<span>15</span>"},
  {key: "k-17", children: "<span>7</span>"},
  {key: "k-4", children: "<span>4</span>"},
  {key: "k-6", children: "<span>6</span>"}
]
// The final node data, because the final node is based on the old node, here is a simulation
let newNodes = JSON.parse(JSON.stringify(preNodes));

//Both indexes are compared from the left
let j = 0;

let preEndIndex = preNodes.length - 1;
let nextEndIndex = nextNodes.length - 1;
let preVNode = preNodes[j];
let nextVNode = nextNodes[j];

outer: {
  while(preVNode.key === nextVNode.key){
    j++;
    if(j> preEndIndex || j > newEndIndex) {
      break outer;
    }
    preVNode = preNodes[j];
    nextVNode = nextNodes[j];
  }
  preVNode = preNodes[preEndIndex];
  nextVNode = nextNodes[nextEndIndex];
  while(preVNode.key === nextVNode.key){
    if(j> preEndIndex || j > nextEndIndex) {
      break outer;
    }
    preVNode = preNodes[--preEndIndex];
    nextVNode = nextNodes[--nextEndIndex];
  }
}
if(j > preEndIndex) {
  // After traversing the old node, the new node still exists. Put the new node into.
  for(let i = j; i< nextEndIndex; i++){
    const addedNode = nextNodes[i];
    // Note: within the framework, the dom is updated with appendchild
    newNodes.splice(i,0,addedNode);
  }
}
else if(j > nextEndIndex){
  // After traversing the new node, the old node still exists. Delete the old node.
  const deleteLen = preEndIndex - j;
  // Note: inside the framework is to rewrite removeChild to update dom
  newNodes.splice(i,deleteLen);
}
else {
  //Do you want to move when saving
  let moved = false;

  const preStart = j;
  const nextStart = j;
  let pos = 0;
  //Save the map of the new node key index to avoid multiple cycles
  const keyInIndex = {}; //{ k-1: 1, k-2: 2, k-3:3,...  }
  //New node length not compared
  const newLength = nextEndIndex - nextStart + 1;
  for(let i = nextStart; i< newLength; i++) {
    keyInIndex[nextNodes[i].key] = i
  }
  const oldLength = preEndIndex - preStart + 1;
  //When there are more old nodes than new nodes, delete the found duplicate nodes
  let patched = 0;
  // Generate an index array of old nodes that can be reused by new nodes
  const noPatchedIndex = Array(newLength).fill(-1); //-1. Status saving
  for(let i = preStart; i< oldLength; i++) {
    const preNode = preNodes[i];
    if(patched <= nextEndIndex) {
      //Save the index corresponding to the old node key in the new node
      const k = keyInIndex[preNode.key];
      if(typeof k !== 'undefined'){
        let idx = k - preStart;
        noPatchedIndex[idx] = i;
        patched++;
        // Filter out elements that need to be moved forward.
        if(k < pos) moved = true;
        else pos = k;
      }else {
        // Dynamic find delete index
        const deleteIndex = newNodes.findIndex(node => node.key === preNodes[i].key)
        newNodes.splice(deleteIndex,1);
      }
    }else {
      newNodes.splice(i,1)
    }
  }
  
  //Handle situations that require movement
  if(moved){
    const newNodesCopy = JSON.parse(JSON.stringify(newNodes))
    //Maximum increment subsequence index
    const lisArr = lis(noPatchedIndex);
    let j = lisArr.length - 1;
    //Traverse the unmatched nodes in the new node and traverse from the back to prevent unexpected changes in the update process.
    for(let i=newLength - 1; i>=0; i--){
      const current = noPatchedIndex[i];
      // Updated physical location
      const pos = i+nextStart;
      let insertPos = newNodes.findIndex(node => node.key === nextNodes[nextStart+lisArr[j]].key);
      if(current === -1){// -1 is the new situation
        // Note [1,2,3] splice(0,4) => [4,1,2,3]
        newNodes.splice(insertPos+1, 0, nextNodes[pos]);
        continue;
      }else if(lisArr[j] !== i) {//Non incremental nodes can be reused
        /*
          insertBefore Function: if the given child node is a reference to an existing node in the document, insertBefore() will move it from the current location to the new location
          The following operation is to implement the insertBefore method.
        */
        //Move the position of the element in the new node
        let oldPos = newNodes.findIndex(node => node.key === nextNodes[pos].key);

        //The inserted node to be deleted corresponds to the original node
        //Insert the position corresponding to the old node in the new node
        newNodes.splice(insertPos+1, 0, newNodes[oldPos]);
        // Judge whether the position of the inserted node is in front of or behind the inserted position. If so, add 1
        oldPos = insertPos > oldPos ? oldPos : oldPos+1;
        newNodes.splice(oldPos, 1)
      }else {
        j--;
      }
    }
  }
}
console.log('newNodes: ', newNodes);
// Find the maximum increment subsequence index
// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
/*
  [3,1,5,4,2] => [1,2]
*/
function lis(arr) {
  const p = arr.slice();
  const result = [0]; // Index array
  let i;
  let j;
  let u;
  let v;
  let c;
  const len = arr.length;
  for (i = 0; i < len; i++) {
    const arrI = arr[i];
    if (arrI !== 0) {
      // Take the last element
      j = result[result.length - 1];
      if (arr[j] < arrI) {
        p[i] = j;
        result.push(i);
        continue;
      }
      u = 0;
      v = result.length - 1;
      //When the result length is greater than 1
      while (u < v) {
        // Take the median
        c = ((u + v) / 2) | 0;
        if (arr[result[c]] < arrI) {
          u = c + 1;
        } else {
          v = c; //The median result is greater than or equal to the current item. v is the median
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1];
        }
        result[u] = i;
      }
    }
  }
  u = result.length;
  v = result[u - 1];
  while (u-- > 0) {
    result[u] = v;
    v = p[v];
  }
  return result;
}

summary

The example in this article is just to better understand the diff idea. There are still some differences between the patch process and the real situation

  • Duplicate node problem. When the new and old nodes have duplicate nodes, the diff function in this paper does not deal with this situation.
  • Only array is used to simulate Vnode. The real Vnode has more parameters than key s and children
  • When comparing the same node, only the key is compared. In fact, it also involves attribute comparison such as class (class name), attrs (attribute value), child node (recursion); In addition, the above children should also be compared. If they are different, they should also be traversed recursively
  • Insert, delete and add an array of nodes I use. In fact, you should use insert before, delete and add. These methods are encapsulated separately, and the corresponding Dom Api cannot be used, because vue is not only used in the browser environment.
  • ...

Vue@3.2 Already out, React@18 It's fast. Hey, I can't finish learning the framework. Let's look at the same things (js, design patterns, data structures, algorithms...)

Hey, comrade, why don't you praise it after reading it? Don't look at others and say you. What do you mean?

reference resources

Standing on other people's shoulders can see further.

[recommended] vue-design
[Nuggets album] Profile Vue JS internal operation mechanism
[Vue patch source address] vue-next ⇲

In addition, the bosses are translating vue3's English documents docs-next-zh-cn

above.

Topics: Vue DOM