How does the diff algorithm of Vue work? This article teaches you

Keywords: Javascript Vue React

preface

This paper aims at the main logic and key details of diff algorithm in vue.

Starting from a simple demo: p tag renders an array of items

<div id="demo">
    <p v-for="item in items" :key="item">{{ item }}</p>
</div>
<script src="../vue-source/dist/vue.js"></script>
<script>
    const app = new Vue({
      el: "#demo",
      data: {
        items: ["a", "b", "c", "d", "e"]
      },
      mounted() {
        setTimeout(() => {
          this.items.splice(2, 0, "f")
        }, 2000)
      }
    })
</script>
Copy code

First, explain the actual sequence:

  1. Item data changes Dep.notify
  2. patch(oldVNode, vnode, ...)
  3. Patch vnode (oldvnode, vnode, insertedVnodeQueue,...) PS: diff started from here. insertedVnodeQueue is a constant defined in the patch function, which has been maintained in the diff in the later period. It is a typical closure structure.
  4. The core method of updateChildren() diff
    5. If you want to learn to practice together with the actual combat, you can get the latest enterprise level Vue3.0/Js/ES6/TS/React/node and other real combat video tutorials in 2020. You can get them in 519293536 for free, and you can't get them in Xiaobai!

sameVnode

sameVnode function runs through the whole diff process, and the first necessary condition is that the key must be equal

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
Copy code

key

As we all know, key plays an important role in patch. Key can effectively reduce unnecessary re rendering in many cases. When the key is not set, the sub element key in the rendering list data is undefined, obviously undefined === undefined. sameVNode is always the same (usually), and it will cause unnecessary rendering (if the key is not set in the first demo, it will cause unnecessary rendering 3 times more).

If the key is set, a.key! = = b.key, the judgment will be terminated immediately. sameVnode directly returns false instead of bb.
Avoid using array subscripts as key s
Because when the array changes, the subscript may also change, which may lead to some hidden bug s.

patch

  • If there is no oldVnode, create elm
  • oldVnode and vnode exist, but sameVnode returns false, then createElm
  • oldVnode and vnode exist, but sameVnode returns true, patchVnode

patchVnode

Vnode can be divided into three types:

  • Plain text Vnode
  • Vnode with Children
  • Vnode without Children

So it can be divided into 3 * 3 situations

  oldVnode.text oldCh !oldCh
vnode.text setTextContent setTextContent setTextContent
ch addVnodes updateChildren addVnodes
!ch setTextContent removeVnodes setTextContent
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
  // If the nodes are the same, they will be returned directly without processing
  if (oldVnode === vnode) {
      return
  }
  // ...
  const elm = vnode.elm = oldVnode.elm
  // ...
  const oldCh = oldVnode.children
  const ch = vnode.children
  // ...
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
    // When new and old Vnode.length All exist and are not equal to enter updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  // ...
}
Copy code

updateChildren

insertedVnodeQueue is an array queue maintained. After diff is completed, the data in the queue will be updated one by one

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // Double pointer
    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
    // First = > last = > first = > last = > traverse old and find index instead
    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, oldEndIdx)
        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]
      }
    }
    // Jumping out of the while loop means that the front and back pointers are interlaced
    // If the pointer of the old node is interleaved first, it means that a new node is added = > addvnodes
    // Otherwise = > removevnodes
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
Copy code

test

Back to the example, in order to get more information, change the demo.

<body>
  <div id="demo">
    <p v-for="item in items" :key="item">{{ item }}</p>
  </div>
  <div>
    ["a", "b", "c", "d", "e", "f", "g"] => ["f", "d", "a", "h", "e", "c", "b", "g"]
  </div>
  <script src="../vue/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: "#demo",
      data: {
        items: ["a", "b", "c", "d", "e", "f", "g"]
      },
      mounted() {
        setTimeout(() => {
          this.items = ["f", "d", "a", "h", "e", "c", "b", "g"]
        }, 2000)
      }
    })
  </script>
</body>
Copy code

stay vue.js updateChildren in for observation.

 

 

First while

Tail gg match successful

 

newStartVnode is g, enter patchVnode. I thought I would judge if( oldVnode.text !== vnode.text )Then I didn't deal with it, but I went back to updateChildren.
Can't help but let me console.log("oldStartIdx", oldStartIdx, oldCh[oldStartIdx])

 

misunderstanding

Where the structure is

 

I thought that VNode.text = a; VNode.children = undefined ...
A in this < p > a < / P > is also a VNode

 

 

Fortunately, the error has been corrected, and the pure text VNode will be ignored.

 

I'm not the only one who knows now

Then add a condition in debugger to observe oldStartVnode.tag === 'p'

 

 

continue

 

 

Second while

The tail head matching is successful. The reference node is a node nodeOps.insertBefore

abcdefg => fabcdeg

oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx];

 

The third while

The first and last four matches are not matched. Enter the code block.

  1. Create key to oldidx (oldch, oldstartidx, oldendidx) for the remaining oldVnode (see the figure below)
  2. If idxInOld does not exist, it means new element = > createElm
  3. If idxInOld exists = > oldCh[idxInOld] = undefined = > reference node executes for a node nodeOps.insertBefore
  4. At this time, the interface also starts from fabcdeg = > fdabceg

 

 

 

 

The fourth while

Successful comparison of the first aa

 

 

The fifth while

The comparison between the first and the last bb is successful. The reference node is g node nodeOps.insertBefore

fdabceg => fdacebg

 

 

Sixth while

The comparison of the first and last cc is successful. The reference node is b node nodeOps.insertBefore

fdacebg => fdaecbg

 

 

The seventh while

At this time, oldStartVnode === undefined = > oldStartVnode = oldCh[++oldStartIdx];

 

 

Eighth while

Tail ee matching succeeded

 

 

At this time, oldendidx < oldstartidx jumps out of while and enters the following code block.

Oldendidx < oldstartidx = > added node = > reference node executes addvnodes() for e node (addvnodes execution finally also nodeOps.insertBefore ), insert the remaining nodes (H) of newCh in front of the E (refElm) node.

 

 

Press F8 once again in debugger to complete the diff process. Now the final fdahecbg is displayed

Reference link

summary

It is strongly recommended to use Chrome browser for debugging. The coordination diagram is not easy to understand

  1. The diff key function updateChildren of rendering list
  2. Note VNode structure = > < p > a < p > = > VNode{tag: 'p', children: VNode{tag: undefined, children: undefined, text: 'a'}, text: undefined}
  3. Each cycle: first = > last = > first = > last = > FindIndex = > createelm (! Idxinold)| nodeOps.insertBefore (idxInOld)
  4. Out of loop = > addvnodes (oldstartidx > oldendidx) | removevnodes (newstartidx > newendidx)

last
Note: if you want to learn to practice together with the actual combat, you can get the latest enterprise class Vue3.0/Js/ES6/TS/React/node and other real combat video tutorials in 2020. You can get them for free in skirt 519293536 if you want to learn, and you can't get them in white!

The text and pictures of this article come from the Internet and my own ideas. They are only for learning and communication. They have no commercial use. The copyright belongs to the original author. If you have any questions, please contact us in time for handling

Posted by feri_soft on Mon, 08 Jun 2020 19:53:08 -0700