One, foreword

1. What is the virtual DOM?

The virtual DOM is a structure that the real DOM is simulated as a tree in the form of an object

Let’s take a look at what the real DOM looks like versus the virtual DOM.

Real Dom

<ul id="list">
    <li class="item">11</li>
    <li class="item">22</li>
    <li class="item">33</li>
</ul>
Copy the code

Corresponding virtual DOM

var oldVDOM = {
  tagName: 'ul',
  props: {
    id: 'list'
  },
  children: [
    { tagName: 'li', props: { class: 'item' }, children: ['11'] },
    { tagName: 'li', props: { class: 'item' }, children: ['22'] },
    { tagName: 'li', props: { class: 'item' }, children: ['33'] },
  ]
}
Copy the code

2. How does vue update nodes when data changes?

Rendering the real DOM is expensive. For example, if we modify a piece of data, rendering it directly to the real DOM will cause the whole DOM tree to redraw and rearrange. Is it possible that we only update the small piece of DOM that we modify instead of updating the whole DOM? Diff’s algorithm can help us.

First, we generate a virtual DOM based on the real DOM. Then, when a node of the Virtual DOM changes, the setter is triggered. Then, all subscribers watcher is notified via dep. notify. Then a new virtual DOM is generated. Finally, the patch method is called to compare the old and new virtual DOM, patch the real DOM while comparing, and update the view.

2. Diff algorithm

The process of comparing the old and new virtual DOM is realized by the Diff algorithm. The diff algorithm only performs the comparison at the same level, not across levels. As shown in the figure below:

The flow chart of the diff

When the data changes, the set method will call dep. notify to notify all subscribers Watcher, subscribers will call patch to patch the real DOM, update the corresponding view.

Second, specific analysis

The function of patch method is to judge whether the virtual nodes of the same layer are of the same type. If so, further comparison will be carried out; otherwise, the whole node will be directly replaced with the real node corresponding to the new virtual DOM

patch

Function patch(oldVnode, newVnode) {if (sameVnode(oldVnode, newVnode)) {// Yes: Continue deep comparison of patchVnode(oldVnode, NewVnode)} else {// no const oldEl = oldvnode.el // real DOM node of the old virtual node const parentEle = api.parentNode(oldEl) // get the parentNode CreateEle (newVnode) // Create the real DOM node corresponding to the new virtual node if (parentEle! == null) {api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))) // Add the new element to the parent element Oldvnode.el) oldVnode = null}} return newVnode}Copy the code

What are the criteria for determining whether a node is of the same type? Let’s look at the sameVnode method next

sameVnode

function sameVnode(oldVnode, NewVnode) {return (oldvNode.key === newVNode.key && // Whether the key values are the same oldvNode. tagName === NewVNode.tagName && // Whether the tag names are the same Oldvnode.iscomment === newvNode.isCOMMENT && // Whether all comment nodes are isDef(oldvNode.data) === isDef(newvNode.data) && // Whether all data is defined SameInputType (oldVnode, newVnode) // When the tag is input, type must be the same)}Copy the code

patchVnode

PatchVnode (oldVnode, vnode) {const el = vnode.el = oldvNode.el El let I, oldCh = oldvNode.children, ch = vnode.children if (oldVnode === vnode) return If (oldvNode.text! == null && vnode.text ! == null && oldVnode.text ! == vnode.text) {api.settextContent (el, vnode.text) == vnode.text) {api.settextContent (el, vnode.text) }else {updateEle(el, vnode, oldVnode) if (oldCh && ch && oldCh! UpdateChildren (el, oldCh, ch)}else if (ch){// Only vnodes have children, CreateEle (Vnode) //create el's children dom else if (oldCh){else if (oldCh){only oldVnode has children, RemoveChildren (el)}}}Copy the code

Code analysis:

  • Find the real dom for oldVnode and name it EL
  • Check whether Vnode and oldVnode point to the same object. If yes, return
  • If they have text nodes and the text is different, set the text node of EL to the text node of Vnode
  • If oldVnode has children and Vnode does not, the children of EL are removed
  • If oldVnode does not have a child node and Vnode does, the child node of EL is added
  • If both have children, it is important to perform the updateChildren function to compare the children

The other points are easy to understand, so let’s talk about updateChildren in detail

updateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode,  newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, Oldstartvnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx]} else {// If (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldStartIdx) OldEndIdx) // Insert newStartIdx into the dom as a new node by determining whether the key of newStartIdx matches in oldKeyToIdx // If the key does not match, // insert newStartIdx into the dom as a new node // OldCh = undefined // oldCh = undefined // oldCh = undefined // oldCh = undefined IdxInOld = isDef(newstartVNode.key)? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined 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) {refElm = isUndef(newCh[newEndIdx + 1])? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, InsertedVnodeQueue) else if (newStartIdx > newEndIdx) {//newOldVnode ends the loop early, oldVnode has left, RemoveVnodes (oldCh, oldStartIdx, oldEndIdx)}}Copy the code

Function function analysis:

  1. We first receive newCh, a child node of Vnode, and oldCh, a child node of oldVnode
  2. Add newStartIdx, newEndIdx, oldStartIdx, oldEndIdx to newCh and oldVnode, and compare them in pairs. There are four ways to compare newCh and oldVnode. If the key is set, the key will be compared. The variable will move toward the center.
  3. If oldStartIdx > oldEndIdx, oldVnode stops traversing early and the remaining Vnode nodes are inserted into the DOM

If newStartIdx > newEndIdx, Vnode ends the traversal early, oldVnode is left, and the remaining oldVnod nodes are deleted from the corresponding DOM

Note: If you can’t match any of the first four, use the key to compare. The specific comparison is as follows:

  1. First, the node corresponding to oldCh’s oldStartIdx and oldEndIdx is fetched and saved as object oldKeyToIdx. The attribute is the node’s key and the attribute value is the node’s subscript I
  2. Select newStartIdx from oldKeyToIdx to match
    • If there is no match, it is a new node and the node is inserted into the real DOM
    • Yes, is it the same type?
      • OldCh [idxInOld] = undefined(oldCh[idxInOld] = undefined)
      • If the node is not of the same type, the node corresponding to newStartIdx will be inserted as the new node

Let’s look at an example

Example 1

Let’s analyze the comparison process

Step 1

  oldS=a oldE=c
  newS=b newE=a
Copy the code

OldS equals newE, so we move the node A to the end, then oldS++, newE–

Step 2

  oldS=b oldE=c
  newS=b newE=e
Copy the code

Comparison result: oldS is equal to newS, so move node B to the first bit, because b is the first bit at this time, do not move, then oldS++, newS++

Step 3

  oldS=c oldE=c
  newS=c newE=e
Copy the code

OldS and oldE are equal to newS, so move the node c to the second digit, and then oldS++, oldE–, newS++

Since oldS > oldE at this time, oldVnode completes traversal in advance, so the remaining E of Vnode needs to be inserted into the third bit. Therefore, the latest DOM is:

Let’s look at another case where the closing variables don’t cross match

Example 2

Let’s analyze the comparison process

Step 1

  oldS=a oldE=c
  newS=b newE=e
Copy the code

Comparison result: No match was found in the four pairwise crossover comparisons, so newS=b was matched with a, B and C in oldS= A to oldE= C, and the result was matched with B. Then the oldVnode’s b is set to undefined(und), so the B in the DOM needs to be moved to the first place. The last newS++

Step 2

  oldS=a oldE=c
  newS=a newE=e
Copy the code

Comparison result: oldS is the same as newS, so a should be moved to the second bit, because a in dom is at the second bit at this time, so it does not move. And then oldS++, newS++

Step 3

  oldS=und oldE=c
  newS=c newE=e
Copy the code

Comparison result: oldE is the same as newS, so c should be moved to the end. At this time, C in DOM is at the end, so it does not move. And finally oldE– newS++

Step 4

  oldS=und oldE=und
  newS=e newE=e
Copy the code

Comparison result: since newS does not match e in newE, insert e into the DOM as a new node, then newS++, newE–

In newS > newE, Vnode first traverses, and oldVnode still has undefined node, delete it in dom, because dom does not have undefined, so do not delete! So the final DOM sort looks like this:

3. Use index as key

When using v-for, why not use index as the key? Take a look at the example below. Insert a new value into li and all four nodes are updated

<ul>                        <ul>
    <li key="0">a</li>        <li key="0">new</li>
    <li key="1">b</li>        <li key="1">a</li>
    <li key="2">c</li>        <li key="2">b</li>
                              <li key="3">c</li>
</ul>                       </ul>
Copy the code

Analysis of execution process:

  1. When patch is executed first, the first node ul of Vnode and oldVnode is compared. They are of the same type, and the pathVnode method is called for further comparison
  2. They all have children, and they are different, so they are executed in the updateChildren method
  3. Since the key=0, 1, 2, 3 cannot be matched by the four matching methods using the closing variable, the key value of the node will be used for comparison (will be executed in the else method).
  4. Because the values of key=0, 1 and 2 in the new and old nodes li are the same, they belong to the same type of nodes, so they will enter the patchVnode and update the nodes in this paper (a->new, b->a, c->b). However, li node with key=3 cannot be found in the old virtual DOM. Is inserted into the real DOM as a new node

How is it different if you use unique ids in li? Here’s an example

<ul> <ul> <li key="104">new</li> <li key="101">a</li> <li key="101">a</li> <li key="102">b</li> <li key="102">b</li> <li  key="103">c</li> <li key="103">c</li> </ul> </ul>Copy the code

Analysis of execution process:

  1. When patch is executed first, the first node ul of Vnode and oldVnode is compared. They are of the same type, and the pathVnode method is called for further comparison
  2. They all have children, and they are different, so they are executed in the updateChildren method
  3. Use closing variables to pairwise cross four matching methods
a b c
  \ \ \
new a b c
Copy the code
In the first time, c matches, the position is the same; in the second time, B matches, the position is the same; in the third time, A matches, the position is the same; finally, the remaining new nodes in the new virtual DOM are reused and inserted into the DOM as new nodesCopy the code

Summary: When using V-for, we must use unique identifiers as key values to save performance

Four, reference

  • Juejin. Cn/post / 684490…
  • Juejin. Cn/post / 699495…
  • Juejin. Cn/post / 699495…