Detailed explanation of virtual DOM and Diff algorithm

Recently, I reviewed virtual DOM and Diff and read many materials. I hereby summarize this long article to deepen my understanding of vue. This article makes a detailed analysis of vue's virtual DOM and Diff algorithm. Some of the key parts are illustrated by moving some diagrams from elsewhere (thanks to the drawing boss), and also contains a more detailed interpretation of the source code.

Rendering of real DOM

Before talking about virtual DOM, let's talk about the rendering of real dom.

The process of browser real DOM rendering is roughly divided into the following parts

  1. Build a DOM tree. Parse and process HTML tags through HTML parser and build them into a DOM tree. When the parser encounters non blocking resources (pictures, css), it will continue to parse. However, if it encounters script tags (especially without async and defer attributes), it will block rendering and stop HTML parsing. This is why it is best to put script tags under the body.
  2. Build CSSOM tree. Similar to building DOM, the browser will also build style rules into CSSOM. The browser will traverse the rule set in CSS and create a node tree with parent-child and brother relationships according to the CSS selector.
  3. Build the Render tree. This step associates DOM with CSSOM to determine what CSS rules should be applied to each DOM element. Match all relevant styles to each visible node in the DOM tree, and determine the calculation style of each node according to the CSS cascade. Invisible nodes (head, including nodes with display:none attributes) will not be generated into the Render tree.
  4. Layout / reflow. The location and size of nodes determined by the browser for the first time is called layout. If the location and size of subsequent nodes change, this step triggers layout adjustment, that is, reflow.
  5. Paint / repaint. Draws each visual part of an element onto the screen, including text, color, border, shadow, and replaced elements (such as buttons and images). Repaint is triggered if these elements change, such as text, color, border, shadow, etc. To ensure that redrawing is faster than the initial drawing, the drawing on the screen is usually broken down into several layers. Promoting the content to the GPU layer (which can be triggered by tranform, filter, will change and opacity) can improve the performance of rendering and redrawing.
  6. Compositing. This step merges the layers in the drawing process to ensure that they are drawn to the screen in the correct order and display the correct content.

Why do you need virtual DOM

The above is a DOM rendering process. If the DOM is updated, the DOM needs to be rendered again. If the following situations exist

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
                ....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName('content');
    for (let i = 0; i < 1000000; i++) {
        content[i].innerHTML = `This is a content${i}`;
        // Trigger backflow
        content[i].style.fontSize = `20px`;
    }
</script>

The DOM needs to be operated for 100w times, and the backflow is triggered for 100w times. Each DOM update will follow the process to update the real DOM without difference. Therefore, it causes a great waste of performance. If there are complex operations in the loop, frequently triggering backflow and redrawing, it is easy to affect the performance and cause jamming. In addition, it should be noted here that virtual DOM does not mean that it is faster than dom. The performance needs to be divided into scenarios. The performance of virtual DOM is positively related to the template size. The comparison process of virtual DOM will not distinguish the size of data. When there are only a few dynamic nodes in the component, the virtual DOM will still traverse the whole vdom, which is one more layer of operation than direct rendering.

    <div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{ item }}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

For example, in the above example, virtual dom. Although there is only one dynamic node, the virtual DOM still needs to traverse the class, text, label and other information of the diff whole list. Finally, it still needs DOM rendering. If it's just a DOM operation, just operate a specific Dom and render it. The core value of virtual DOM is that it can describe the real DOM through js, with stronger expressiveness. Through declarative language operation, it provides developers with a more convenient and fast development experience. Moreover, without manual optimization, it ensures the lower limit of performance and higher performance price ratio in most scenarios.

Virtual DOM

Virtual DOM is essentially a js object, which represents the real DOM structure. tag is used to describe tags, props is used to describe attributes, and children is used to represent nested hierarchical relationships.

const vnode = {
    tag: 'div',
    props: {
        id: 'container',
    },
    children: [{
        tag: 'div',
        props: {
            class: 'content',
        },
          text: 'This is a container'
    }]
}

//Corresponding real DOM structure
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>

The update of the virtual DOM will not operate the DOM immediately, but will find out the nodes to be updated through the diff algorithm, update as needed, and save the updated content as a js object. After the update is completed, it will be mounted on the real DOM to realize the real DOM update. Through virtual DOM, three problems of operating real DOM are solved.

  1. Frequent updates without differences lead to frequent updates of DOM, resulting in performance problems
  2. Frequent reflow and redraw
  3. Development experience

In addition, because the virtual DOM stores js objects, it naturally has the ability of cross platform, not just limited to the browser.

advantage

To sum up, the advantages of virtual DOM are as follows

  1. The DOM does not need to be updated frequently for minor modifications. The diff algorithm of the framework will automatically compare and analyze the nodes that need to be updated, and update them as needed
  2. Updating data will not cause frequent backflow and redrawing
  3. Stronger expression and more convenient data update
  4. It saves js objects and has cross platform capability

Insufficient

Virtual DOM also has disadvantages. When rendering a large number of DOM for the first time, due to the calculation of an additional layer of virtual DOM, it will be slower than innerHTML insertion.

Implementation principle of virtual DOM

It is mainly divided into three parts

  1. Create node description objects through js
  2. diff algorithm compares and analyzes the differences between the old and new virtual DOM S
  3. Update the differential patch to the real dom

Diff algorithm

In order to avoid unnecessary rendering and update on demand, the virtual DOM will use the Diff algorithm to compare the virtual DOM nodes and compare the node differences, so as to determine the nodes to be updated, and then render. vue adopts the strategy of depth first and same layer comparison.

The comparison between the new node and the old node mainly focuses on three things to achieve the purpose of rendering

  1. Create a new node
  2. Delete obsolete node
  3. Update existing nodes

How to compare whether the old and new nodes are consistent?

function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.asyncFactory === b.asyncFactory && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b) //Processing of input node
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

//Judge whether two nodes are of the same input type
function sameInputType(a, b) {
    if (a.tag !== 'input') return true
    let i
    const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
    const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
    //input type is the same or both types are text
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

It can be seen that whether the two nodes are the same requires comparison of tags, attributes (in vue, data is used to represent the attribute props in vnode) and annotation nodes (isComment). In addition, special processing will be performed when encountering input.

Create a new node

When there are new nodes and there are no old nodes, it means that this is a new content node. Only element nodes, text nodes and annotation nodes can be created and inserted into the DOM.

Delete old node

When the old node has and the new node does not, it means that the new node abandons part of the old node. Deleting a node will delete the child nodes of the old node.

Update node

If both the new node and the old node exist, the new node shall prevail and the old node shall be updated. How to determine whether a node needs to be updated?

  • Judge whether the new node is completely consistent with the old node. If it is the same, it does not need to be updated
  // Judge whether vnode and oldVnode are exactly the same
  if (oldVnode === vnode) {
    return;
  }
  • Judge whether the new node and the old node are static nodes, whether the key is the same, whether it is a clone node (if it is not a clone node, it means that the rendering function has been reset and needs to be re rendered at this time), or whether the once attribute is set. If the conditions are met, replace the componentInstance
  // Whether it is a static node, whether the key is the same, whether it is a clone node, or whether the once attribute is set
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  • Judge whether the new node has text (through the text attribute). If there is text, compare the old nodes at the same level. If the old node text is different from the new node text, adopt the new text content. If the new node has no text, you need to judge the relevant conditions of the child nodes later
//Judge whether the new node has text
if (isUndef(vnode.text)) {
  //If there is no text, process the relevant code of the child node
  ....
} else if (oldVnode.text !== vnode.text) {
  //Replace old node text with new node text
  nodeOps.setTextContent(elm, vnode.text)
}
  • Judge the correlation between the new node and the child nodes of the old node. Here can be divided into four cases

    1. Both the new node and the old node have child nodes
    2. Only the new node has children
    3. Only the old node has child nodes
    4. Neither the new node nor the old node has child nodes

All have child nodes

When there are child nodes, you need to compare the old and new nodes. If they are different, you need to perform a diff operation. In vue, this is the updateChildren method. We will talk about it in detail later. The comparison of child nodes is mainly a two terminal comparison.

//Judge whether the new node has text
if (isUndef(vnode.text)) {
    //When both the old and new nodes have child nodes, if the new and old child nodes are different, the updateChildren method is used to compare the child nodes
    if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
} else if (oldVnode.text !== vnode.text) {
    //Replace old node text with new node text
    nodeOps.setTextContent(elm, vnode.text)
}

Only the new node has children

If only the new node has child nodes, it means that this is a new content. That is, add a child node to the DOM. Before adding a new node, a duplicate key detection will be performed and a reminder will be given. At the same time, if the old node is only a text node and has no child nodes, the text content of the old node needs to be cleared.

//Only the new node has children
if (isDef(ch)) {
  //Check for duplicate key s
  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(ch)
  }
  //Clear old node text
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
  //Add new node
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}

//Check for duplicate key s
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      //Each Key of child node
      const key = vnode.key
      if (isDef(key)) {
          if (seenKeys[key]) {
              warn(
                  `Duplicate keys detected: '${key}'. This may cause an update error.`,
                  vnode.context
              )
          } else {
              seenKeys[key] = true
          }
      }
  }
}

Only the old node has child nodes

Only the old node has, which means that the new node discards the child nodes of the old node, so the child nodes of the old node need to be deleted

if (isDef(oldCh)) {
  //Delete old node
  removeVnodes(oldCh, 0, oldCh.length - 1)
}

There are no child nodes

At this time, you need to judge the old node text to see whether the old node has text. If so, it will be cleared

if (isDef(oldVnode.text)) {
  //empty
  nodeOps.setTextContent(elm, '')
}

The overall logic code is as follows

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
) {
    // Judge whether vnode and oldVnode are exactly the same
    if (oldVnode === vnode) {
        return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // Clone reuse node
        vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
        if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
        } else {
            vnode.isAsyncPlaceholder = true
        }
        return
    }
        // Whether it is a static node, whether the key is the same, whether it is a clone node, or whether the once attribute is set
    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)) {
          //Call the update callback and the update hook
        for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
        //Judge whether the new node has text
    if (isUndef(vnode.text)) {
          //When both the old and new nodes have child nodes, if the new and old child nodes are different, the updateChildren method is used to compare the child nodes
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
              //Only the new node has children
            if (process.env.NODE_ENV !== 'production') {
                  //Duplicate Key detection
                checkDuplicateKeys(ch)
            }
              //Clear old node text
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
              //Add new node
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
              //Only the old node has child nodes. Delete the old node
            removeVnodes(oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
              //Both old and new nodes have no child nodes
            nodeOps.setTextContent(elm, '')
        }
    } else if (oldVnode.text !== vnode.text) {
          //Replace old node text with new node text
        nodeOps.setTextContent(elm, vnode.text)
    }

    if (isDef(data)) {
        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

The flow chart will be clearer

Comparison update of child nodes updateChildren

When both old and new nodes have child nodes, you need to call the updateChildren method to compare and update child nodes. Therefore, in the data, the old and new node sub nodes are saved as two arrays.

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];

The child node update adopts the strategy of double ended comparison. What is double ended comparison? That is, the comparison between the old and new nodes is the strategy of comparing the head and tail elements (there are four kinds of comparison) with each other, and then moving closer to the middle (newStartIdx, increasing with oldStartIdx, and decreasing with oldEndIdx).

Comparison process

Move closer to the middle

Here is a description of the new front, new rear, old front and old rear

  1. New front refers to the first element in the array of unprocessed child nodes of the new node, which corresponds to the newStartVnode in the vue source code
  2. After new, it refers to the last element in the array of unprocessed child nodes of the new node, which corresponds to newEndVnode in the vue source code
  3. Before, it refers to the first element in the array of unprocessed child nodes of the old node, which corresponds to oldStartVnode in vue source code
  4. After the old node, it refers to the last element in the array of unprocessed child nodes of the old node, which corresponds to oldEndVnode in vue source code

Child node comparison process

Next, the above comparison process and the operations after comparison will be described

  • Compare the new node with the old one. If they are the same, update the patchVnode mentioned above, and then step back between the old and new nodes to compare the second item We don't change the way of comparison until we encounter different situations

if (sameVnode(oldStartVnode, newStartVnode)) {
  // Update child nodes
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // One step back for the old and the new
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
  • For the comparison between the new and the old, if they are the same, update pathvnode, and then move forward to compare the former The comparison method will not be changed until there is a difference

if (sameVnode(oldEndVnode, newEndVnode)) {
    //Update child nodes
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // Old and new forward
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • Compare the new front with the old one. If they are the same, update them, and then move the old front to the back of all unprocessed old node arrays to keep the positions of the old front and the new rear consistent. Then both sides move closer to the middle, the new forward and the old backward. If it is different, it will continue to switch the comparison mode

if (sameVnode(oldStartVnode, newEndVnode)) {
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  //Move and insert the first child node of the old child node array to the last
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  //Old backward
  oldStartVnode = oldCh[++oldStartIdx]
  //New forward
  newEndVnode = newCh[--newEndIdx]
  • The comparison between the new front and the old rear. If they are the same, update them, and then move the old rear to the front of all unprocessed old node arrays. The old rear is consistent with the new front position, and then the new back, the old forward, continue to move closer to the middle. Continue comparing the remaining nodes. If not, the traditional loop traversal lookup is used.

if (sameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  //Insert old move back to front
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  //Old forward
  oldEndVnode = oldCh[--oldEndIdx]
  //New backward
  newStartVnode = newCh[++newStartIdx]
}
  • Loop through the search. If none of the above four are found, the key will be used to find the match.

At this step, for nodes that do not have a key set, the mapping between key and index {key:index} will be established through createKeyToOldIdx for the first time

// For nodes that do not have a key set, the mapping between key and index {key:index} will be established through createKeyToOldIdx for the first time
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

Then compare the key of the new node with the old node to find the location of the node whose key value matches. Note that if the new node does not have a key, the findIdxInOld method will be executed to traverse the matching old node from beginning to end

//Find the location subscript of the new node in the old node through the key of the new node. If the key is not set, the traversal operation will be performed
idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

//findIdxInOld method
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    //Same node subscript found
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

If the subscript matching the new node and the old node is still not found through the above method, it indicates that this node is a new node, and then perform the new operation.

//If the new node cannot find its own location subscript in the old node, it indicates that it is a new element and performs the add operation
if (isUndef(idxInOld)) {
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}

If it is found, it means that an old node with the same key value or the same node and key is found in the old node. If the nodes are the same, after patchVnode, you need to move the old node to all unprocessed nodes. For nodes with the same key and different elements, they are regarded as new nodes and add them. After execution, the new node moves back one step.

//If the new node cannot find its own location subscript in the old node, it indicates that it is a new element and performs the add operation
if (isUndef(idxInOld)) {
  // New element
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  // A node with the same key value was found in the old node
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    // Same child node update operation
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // After updating, assign the old node undefined
    oldCh[idxInOld] = undefined
    //Move old nodes before all unprocessed nodes
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  } else {
    // If it is the same key, different elements will be created as new nodes
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
  }
}
//New node backward
newStartVnode = newCh[++newStartIdx]

When the traversal of the old node is completed, but the traversal of the new node has not been completed, it means that the subsequent nodes are new nodes and perform the new operation. If the traversal of the new node is completed and the traversal of the old node has not been completed, it means that the old node has redundant nodes and performs the deletion operation.

//Complete the traversal of the old node, but the new node has not completed the traversal,
if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  // New node
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
  // Redundant old nodes are found and deleted
  removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}

Child node comparison summary

The above is a complete process of comparing and updating child nodes, which is a complete logic code

function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0] //Before
    let oldEndVnode = oldCh[oldEndIdx] //Old post
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0] //New front
    let newEndVnode = newCh[newEndIdx] //New post
    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)
    }

    //Two terminal comparison traversal
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            //The old front moves backward
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
        } else if (isUndef(oldEndVnode)) {
            // Old rear moving forward
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            //New and old
            //Update child nodes
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                // One step back for the old and the new
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            //New and old
            //Update child nodes
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //Take a step forward for the old and the new
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // New and old
            //Update child nodes
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //Insert old move to last
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
                //New forward, old backward
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // New front and old back
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)

            //Insert old move back to front
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)

            //New backward, old forward
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
            // For nodes that do not have a key set, the mapping between key and index {key:index} will be established through createKeyToOldIdx for the first time
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            //Find the location subscript of the new node in the old node through the key of the new node. If the key is not set, the traversal operation will be performed
            idxInOld = isDef(newStartVnode.key) ?
                oldKeyToIdx[newStartVnode.key] :
                findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

            //If the new node cannot find its own location subscript in the old node, it indicates that it is a new element and performs the add operation
            if (isUndef(idxInOld)) {
                // New element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
                // A node with the same key value was found in the old node
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    // Same child node update operation
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                        // After updating, assign the old node undefined
                    oldCh[idxInOld] = undefined
                        //Move old nodes before all unprocessed nodes
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // If it is the same key, different elements will be created as new nodes
                    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
                }
            }
            //New node one step back
            newStartVnode = newCh[++newStartIdx]
        }
    }

    //Complete the traversal of the old node, but the new node has not completed the traversal,
    if (oldStartIdx > oldEndIdx) {
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
            // New node
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
        // Redundant old nodes are found and deleted
        removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
}

reference material

  1. VirtualDOM and diff
  2. Rendering pages: how browsers work
  3. DOM diff in Vue
  4. In depth analysis: virtual DOM of Vue core

Keywords: Javascript Front-end Vue.js source code

Added by jeankaleb on Fri, 17 Dec 2021 09:55:43 +0200