[Vue2.0 source code learning] Virtual DOM articles - DOM in Vue - optimize and update child nodes

1 Introduction

In the previous article, we introduced that when both the new VNodeand the old oldVNodeare element nodes and both contain child nodes, Vuethe child nodes are

First the outer loop newChildrenarray, and then the inner loop oldChildrenarray, every time newChildrena child node in the outer array is looped, go to the inner oldChildrenarray to see if there is a child node that is the same as it, and finally make different operations according to different situations.

At the end of the previous article, we also said that although this method can solve the problem, there are still places that can be optimized. For example, when the number of child nodes is large, the time complexity of the loop algorithm will become very large, which is not conducive to performance improvement. Of course, VueI also realized this and optimized it, so in this article, let’s learn Vuehow to optimize the update of child nodes.

2. Introduction to Optimization Strategies

Suppose we have a new newChildrenarray and an old oldChildrenarray, as follows:

newChildren = ['新子节点1','新子节点2','新子节点3','新子节点4']
oldChildren = ['旧子节点1','旧子节点2','旧子节点3','旧子节点4']

If we follow the solution before optimization, then our next operation should be like this: first loop through newChildrenthe array, get the first new child node 1, and then use the first new child node 1 to follow the oldChildrenold child node in the array Comparing one by one, if luck is a little bit better, oldChildrenthe first old child node 1 in the array happens to be the same as the first new child node 1, then everyone is happy, and they can be processed directly without further looping down. Then if the luck is a bit bad, until oldChildrenthe fourth old child node 4 in the loop is the same as the first new child node 1, then it will loop 4 more times at this time. We might as well imagine the situation to be a little more extreme. If neither newChildrenthe array nor oldChildrenthe first three nodes in the array have changed, but only the fourth node has changed, then we will loop 16 times and only find out at the 16th loop The new node 4 is the same as the old node 4 and needs to be updated, as shown in the figure below:
insert image description here

In the above example, there are only four child nodes, and it seems that there are no defects. However, when the number of child nodes is large, the time complexity of the algorithm will be very high, which is not conducive to performance improvement.

So how can we optimize it? In fact, we can think of it this way, instead of looping through the two arrays in order newChildren, oldChildrenwe can first compare the child nodes at special positions in the two arrays, for example:

  • First compare the newChildrenfirst child nodes of all unprocessed child nodes in the array with the first child nodes of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating nodes;oldChildren
  • If they are different, compare newChildrenthe last child node of all unprocessed child nodes in the array with the oldChildrenlast child node of all unprocessed child nodes in the array. If they are the same, then directly enter the operation of updating the node;
  • If they are different, compare the newChildrenlast child node of all unprocessed child nodes in the array with the first child node of all unprocessed child nodes in the array. If they are the same, then go directly to the update node operation. After the update oldChildrenThen oldChildrenmove the node in the array to newChildrenthe same position as the node in the array;
  • If they are different, compare newChildrenthe first child node of all unprocessed child nodes in oldChildrenthe array with the last child node of all unprocessed child nodes in the array. If they are the same, then go directly to the update node operation. After the update is complete Then oldChildrenmove the node in the array to newChildrenthe same position as the node in the array;
  • After trying the last four cases, if they are still different, then find the nodes according to the previous loop method.

The process is shown in the figure below:

insert image description here

In the image above, we put:

  • newChildrenThe first child node of all unprocessed child nodes in the array is called: new before;
  • newChildrenThe last child node of all unprocessed child nodes in the array is called: new post;
  • oldChildrenThe first child node of all unprocessed child nodes in the array is called: old front;
  • oldChildrenThe last child node of all unprocessed child nodes in the array is called: old post;

OK, now that we have the above concepts, let's take a look at how it is implemented.

3. New Front vs. Old Front

Compare newChildrenthe first child node of all unprocessed child nodes in the array with oldChildrenthe first child node of all unprocessed child nodes in the array. If they are the same, that's great. Go directly to the update node mentioned in the previous article and since the positions of the new and old nodes are the same, there is no need to perform node movement operations; if they are different, it does not matter, and then try the next three cases.insert image description here

4. The new post and the old post

Compare newChildrenthe last child node of all unprocessed child nodes in the array with the oldChildrenlast child node of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating the node and because the new one and the old one The positions of the nodes are also the same, so there is no need to move the nodes; if they are different, continue to try later.insert image description here

5. New after vs old front

Compare newChildrenthe last child node of all unprocessed child nodes in the array with oldChildrenthe first child node of all unprocessed child nodes in the array, if they are the same, then directly enter the operation of updating the node, and then update the oldChildrenarray Move the node to newChildrenthe same position as the node in the array;
insert image description here

At this time, the operation of moving the node appears. The most critical part of moving the node is to find the location to be moved. We have repeatedly emphasized that updating nodes should be VNodebased on the new ones, and then operate the old ones oldVNodeso that the old ones are the same oldVNodeas the new onesVNode . Then the current situation is: newChildrenthe last child node in the array oldChildrenis the same as the first child node in the array, then we should oldChildrenmove the first child node to the position of the last child node in the array, as shown in the figure below:

insert image description here

It is not difficult to see from the figure that we need to oldChildrenmove the first child node in the array after all unprocessed nodes in the array .

If after the comparison, it is found that the two nodes are still not the same node, then continue to try the last case.

6. New before and old after

Compare newChildrenthe first child node of all unprocessed child nodes in the array with oldChildrenthe last child node of all unprocessed child nodes in the array. If they are the same, then go directly to the operation of updating the node, and then update the node in the oldChildrenarray Move the node to newChildrenthe same position as the node in the array;

insert image description here

Similarly, the logic of node moving position in this case is similar to the logic of "new after and old before", that is, the newChildrenfirst child node in the array oldChildrenis the same as the last child node in the array, then we should oldChildrenput in the array The last child node moves to the position of the first child node, as shown in the figure below:

insert image description here

It is not difficult to see from the figure that we need to oldChildrenmove the last child node in the array before all unprocessed nodes in the array .

OK, the above are the 4 cases of sub-node comparison and update optimization strategies. If the same node is not found after trying the above 4 cases one by one, then search through the previous loop method.

7. Go back to the source code

After analyzing the ideas and clarifying the logic, let's go back to the source code to see if the logic implemented by the source code is the same as what we analyzed. The source code is as follows:

// 循环更新子节点
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    
    
    let oldStartIdx = 0               // oldChildren开始索引
    let oldEndIdx = oldCh.length - 1   // oldChildren结束索引
    let oldStartVnode = oldCh[0]        // oldChildren中所有未处理节点中的第一个
    let oldEndVnode = oldCh[oldEndIdx]   // oldChildren中所有未处理节点中的最后一个

    let newStartIdx = 0               // newChildren开始索引
    let newEndIdx = newCh.length - 1   // newChildren结束索引
    let newStartVnode = newCh[0]        // newChildren中所有未处理节点中的第一个
    let newEndVnode = newCh[newEndIdx]  // newChildren中所有未处理节点中的最后一个

    let oldKeyToIdx, idxInOld, vnodeToMove, 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

    if (process.env.NODE_ENV !== 'production') {
    
    
      checkDuplicateKeys(newCh)
    }

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
      if (isUndef(oldStartVnode)) {
    
    
        oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
      } else if (isUndef(oldEndVnode)) {
    
    
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
        // 如果新前与旧前节点相同,就把两个节点进行patch更新
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
        // 如果新后与旧后节点相同,就把两个节点进行patch更新
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
    
     // Vnode moved right
        // 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
        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
        // 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
    
    
        // 如果不属于以上四种情况,就进行常规的循环比对patch
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果在oldChildren里找不到当前循环的newChildren里的子节点
        if (isUndef(idxInOld)) {
    
     // New element
          // 新增节点并插入到合适位置
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
    
    
          // 如果在oldChildren里找到了当前循环的newChildren里的子节点
          vnodeToMove = oldCh[idxInOld]
          // 如果两个节点相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
    
    
            // 调用patchVnode更新节点
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            // canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
    
    
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
    
    
      /**
       * 如果oldChildren比newChildren先循环完毕,
       * 那么newChildren里面剩余的节点都是需要新增的节点,
       * 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
       */
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
    
    
      /**
       * 如果newChildren比oldChildren先循环完毕,
       * 那么oldChildren里面剩余的节点都是需要删除的节点,
       * 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
       */
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

Before reading the source code, we first have such a concept: that is, in the optimization strategy we mentioned earlier, nodes may be compared from the front or from the back, and the update process will be performed if the comparison is successful, that is to say, we It is possible to process the first one, and it is also possible to process the last one, so when we cycle, we cannot simply cycle from front to back or from back to front, but from both sides to the middle.

So how to loop from both sides to the middle? Please see the picture below:

insert image description here

First, we prepare 4 variables:

  • newStartIdx:newChildren the subscript of the start position in the array;
  • newEndIdx:newChildren the subscript of the end position in the array;
  • oldStartIdx:oldChildren the subscript of the starting position in the array;
  • oldEndIdx:oldChildren the subscript of the end position in the array;

When looping, each time a node is processed, the subscript is moved to the direction indicated by the arrow in the figure. After the node represented by the start position is processed, it is moved backward by one position; the node represented by the end position is After processing, move forward one position; since our optimization strategy is to update the old and new nodes in pairs, one update will move two nodes. To put it more bluntly: newStartIdxand oldStartIdxcan only move backward (only add), newEndIdxand oldEndIdxcan only move forward (only subtract).

When the start position is greater than the end position, it means that all nodes have been traversed.

OK, with this concept, we start to read the source code:

  1. If oldStartVnodeit does not exist, skip it directly, oldStartIdxadd 1, and compare the next

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          
          
     if (isUndef(oldStartVnode)) {
          
          
       oldStartVnode = oldCh[++oldStartIdx]
     }
    }
    
  2. If oldEndVnodeit does not exist, it will be skipped directly, and oldEndIdx1 will be subtracted, compared with the previous one

    else if (isUndef(oldEndVnode)) {
          
          
        oldEndVnode = oldCh[--oldEndIdx]
    }
    
  3. If the new front node is the same as the old front node, update the two nodes patch, add 1 to the sum at the same time , oldStartIdxand newStartIdxmove one position back

    else if (sameVnode(oldStartVnode, newStartVnode)) {
          
          
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
    }
    
  4. If the new node is the same as the old node, update the two nodes patch, decrease the sum by 1, oldEndIdxand newEndIdxmove one position forward

    else if (sameVnode(oldEndVnode, newEndVnode)) {
          
          
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  5. If the new node is the same as the old node, first update the two nodes patch, then move the old node oldChilrenbehind all the unprocessed nodes, and finally oldStartIdxadd 1, move back one position, newEndIdxsubtract 1, and move forward one position

    else if (sameVnode(oldStartVnode, newEndVnode)) {
          
          
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  6. If the new front node is the same as the old back node, first update the two nodes patch, then move the old back node to oldChilrenthe front of all unprocessed nodes, and finally add newStartIdx1, move back one position, oldEndIdxsubtract 1, and move forward one position

    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]
    }
    
  7. If it does not belong to the above four situations, perform a regular cycle comparisonpatch

  8. If in the loop, oldStartIdxit is greater oldEndIdxthan, it means that the cycle is completed oldChildrenthan newChildrenbefore, then newChildrenthe remaining nodes in it are nodes that need to be added, and [newStartIdx, newEndIdx]all the nodes in between are inserted into DOMthe

    if (oldStartIdx > oldEndIdx) {
          
          
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    }
    
  9. If in the cycle, newStartIdxit is greater newEndIdxthan, it means that the cycle is completed newChildrenthan oldChildrenbefore, then oldChildrenthe remaining nodes in it are the nodes that need to be deleted, and [oldStartIdx, oldEndIdx]all the nodes in between are deleted

    else if (newStartIdx > newEndIdx) {
          
          
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
    

OK, the processing is complete. It can be seen that the processing logic in the source code is the same as the logic we analyzed before.

8. Summary

In this article, we introduced Vuethe optimization strategy for neutron node update, and found that Vuein order to avoid the performance problems caused by the large amount of data in the double loop and the increase in time complexity, we chose to compare the four special positions in the sub-node array with each other. Yes, they are: new front and old front, new rear and old rear, new rear and old front, new front and old rear. For each case, we analyzed its logic in the form of graphics and text. Finally, we go back to the source code and verify that our analysis is correct by reading the source code. Fortunately, we have found the corresponding implementation in the source code for each step of our previous analysis, which can verify that our analysis is correct. The above is the process Vuein the middle patch, that is, DOM-Diffall the contents of the algorithm. I believe you will have a clearer idea when you read this part of the source code again.

Guess you like

Origin blog.csdn.net/weixin_46862327/article/details/130899256