The diff algorithm in vue (detailed explanation)

Interviewer: Do you understand Vue's diff algorithm? Tell me

1. What is it?

diffThe algorithm is an efficient algorithm for comparing tree nodes at the same level

It has two characteristics:

  • Comparisons will only be performed at the same level, not cross-level comparisons
  • In the process of diff comparison, the loop compares from both sides to the middle

diffAlgorithms are applied in many scenarios. vueIn , it acts on the comparison of old and new nodes domrendered from virtual to domrealVNode

2. Comparison method

diffThe overall strategy is: depth first, same layer comparison

  1. Comparisons will only be performed at the same level, not cross-level comparisons
img
  1. During the comparison, the loop is closed from both sides to the middle
img

Here is an example of an vuealgorithm diffupdate:

The old and new VNodenodes are shown in the figure below:

After the first cycle, it is found that the old node D is the same as the new node D, and the old node D is directly reused as the difffirst real node, and the old node endIndexis moved to C, and the new node is startIndexmoved to C

After the second cycle, the end of the old node is also the same as the beginning of the new node (both are C). Similarly, the diffreal node of C created later is inserted behind the node of D created for the first time. At the same time, the old node endIndexmoves to B, and the new node startIndexmoves to E

In the third cycle, it is found that E is not found. At this time, a new real node E can only be created directly and inserted after the C node created for the second time. At the same time the new node startIndexmoves to A. startIndexThe and of the old node endIndexremain unchanged

In the fourth cycle, it is found that the beginnings of the old and new nodes (both are A) are the same, so the real node of A is created difflater , and inserted behind the E node created last time. At the same time, the old node startIndexmoves to B, and the new node startIndexmoves to B

In the fifth cycle, the situation is the same as the fourth cycle, so the B real node created difflater inserted behind the A node created last time. At the same time, the old node startIndex moved to C, and the startIndex of the new node moved to F

The new node startIndexis already greater endIndexthan , you need to newStartIdxcreate newEndIdxall the nodes between and , that is, node F, directly create the real node corresponding to the F node and put it behind the B node

3. Principle Analysis

When the data changes, setthe method will be called to Dep.notifynotify all subscribers Watcher, and the subscribers will call patchto patch the real DOMone and update the corresponding view

Source location: src/core/vdom/patch.js

function patch(oldVnode, vnode, hydrating, removeOnly) {
    
    
    if (isUndef(vnode)) {
    
     // 没有新节点,直接执行destory钩子函数
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
    
    
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
    } else {
    
    
        const isRealElement = isDef(oldVnode.nodeType)
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
    
    
            // 判断旧节点和新节点自身一样,一致执行patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
        } else {
    
    
            // 否则直接销毁及旧节点,根据新节点生成dom元素
            if (isRealElement) {
    
    

                if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    
    
                    oldVnode.removeAttribute(SSR_ATTR)
                    hydrating = true
                }
                if (isTrue(hydrating)) {
    
    
                    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
    
    
                        invokeInsertHook(vnode, insertedVnodeQueue, true)
                        return oldVnode
                    }
                }
                oldVnode = emptyNodeAt(oldVnode)
            }
            return vnode.elm
        }
    }
}

patchThe first two parameters of the function are oldVnodeand Vnode, which represent the new node and the old node respectively, and mainly make four judgments:

  • There is no new node, directly trigger destorythe hook of the old node
  • There are no old nodes, which means that it is when the page is just initialized. At this time, there is no need to compare at all, and all are newly created, so only callcreateElm
  • The old node is the same as the new node itself, by sameVnodejudging whether the nodes are the same, if they are the same, directly call patchVnode to process the two nodes
  • The old node is different from the new node itself. When the two nodes are different, directly create a new node and delete the old node

The following is mainly about patchVnodethe part

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    
    
    // 如果新旧节点一致,什么都不做
    if (oldVnode === vnode) {
    
    
      return
    }

    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
    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
    }
    // 如果新旧都是静态节点,并且具有相同的key
    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
    // 也不用再有其他操作
    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)) {
    
    
      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)
    }
    // 如果vnode不是文本节点或者注释节点
    if (isUndef(vnode.text)) {
    
    
      // 并且都有子节点
      if (isDef(oldCh) && isDef(ch)) {
    
    
        // 并且子节点不完全一致,则调用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        // 如果只有新的vnode有子节点
      } else if (isDef(ch)) {
    
    
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // elm已经引用了老的dom节点,在老的dom节点上添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
      } else if (isDef(oldCh)) {
    
    
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)

        // 如果老节点是文本节点
      } else if (isDef(oldVnode.text)) {
    
    
        nodeOps.setTextContent(elm, '')
      }

      // 如果新vnode和老vnode是文本节点或注释节点
      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
    
    
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
    
    
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patchVnodeMainly made several judgments:

  • Whether the new node is a text node, if so, domthe text content of the direct update is the text content of the new node
  • If both the new node and the old node have child nodes, then compare and update the child nodes
  • Only the new node has child nodes, and the old node does not, so there is no need to compare, all nodes are brand new, so just create all new ones directly, new creation refers to creating all new nodes and adding them to the parent DOMnode
  • Only the old node has child nodes but the new node does not, which means that all the old nodes are gone in the updated page, so what to do is to delete all the old nodes, that is, delete DOMdirectly

If the child nodes are not exactly the same, callupdateChildren

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    
    
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    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

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
    
    
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
    
    
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
    
     // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
    
     // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不匹配
      } else {
    
    
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) {
    
     // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
    
    
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
    
    
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
    
    
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
    
    
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }

whileThe loop mainly handles the following five scenarios:

  • When the new and old VNodenodes startare the same, directly patchVnode, and the start index of the new and old VNodenodes are both increased by 1
  • When the old and new VNodenodes endare the same, it is also direct patchVnode, and at the same time the end index of the old and new VNodenodes are both reduced by 1
  • When the of the old VNodenode is the same as that of startthe new VNodenode end, at this patchVnodetime , the current real domnode needs to be moved oldEndVnodeto the back of , at the same time, VNodethe start index of the old node is increased by 1, and the end index of the new VNodenode is decreased by 1
  • When the of the old VNodenode is the same as that of endthe new VNodenode start, patchVnodeafter , the current real domnode needs to be moved oldStartVnodeto the front of , and VNodethe end index of the old node is decreased by 1, and the start index of the new VNodenode is increased by 1
  • If none of the above four situations are met, it means that there are no identical nodes that can be reused, and it will be divided into the following two situations:
    • Find the VNodeold node that is consistent keyvalue is the value of and the corresponding indexsequence value, and then proceed , and at the same time move to the front of the corresponding realvaluenewStartVnodekeyVNodepatchVnode dom oldStartVnodedom
    • Call to createElmcreate a new domnode and put it at the newStartIdxcurrent position

summary

  • When the data changes, the subscriber watcherwill call patchthe real DOMpatch
  • By isSameVnodemaking a judgment, if it is the same, call patchVnodethe method
  • patchVnodedid the following:
    • Find the corresponding truth dom, calledel
    • If both have text nodes and are not equal, set elthe text node to Vnodethe text node
    • If oldVnodethere is a child node and VNodethere is no child node, delete elthe child node
    • If there is oldVnodeno child node VNode, then VNodethe child node will be realized and added toel
    • If both have child nodes, execute updateChildrenfunction compare child nodes
  • updateChildrenMainly did the following operations:
    • Set the old and new VNodehead and tail pointers
    • Compare the old and new head and tail pointers, move closer to the middle in a loop, call to repeat the process according to the situation patchVnode, patchcall to createElemcreate a new node, find a keyconsistent VNodenode from the hash table, and then divide the situation into operations

references

  • https://juejin.cn/post/6881907432541552648#heading-1
  • https://www.infoq.cn/article/udlcpkh4iqb0cr5wgy7f

Guess you like

Origin blog.csdn.net/qq_43375584/article/details/125441938