vue2 diff algorithm

what is diff

The diff algorithm is an efficient algorithm for comparing tree nodes at the same level.
It has two characteristics:
♥Comparison will only be performed at the same level, not cross-level comparison
♥During the process of diff comparison, the loop compares from both sides to the middle
The diff algorithm is applied in many scenarios. In vue, it acts on the comparison of old and new VNode nodes rendered from virtual dom to real dom

comparison method

The overall strategy of diff is: depth first, same-level comparison comparison
will only be performed at the same level, and no cross-level comparison will be performed

   

 During the comparison, the loop is closed from both sides to the middle

The old and new VNode nodes 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 first real node after the diff. At the same time, the endIndex of the old node is moved to C, and the startIndex of the new node is moved to C

 After the second cycle , the end of the old node is also the same as the beginning of the new node (both C). Similarly, the real node created by C after diff is inserted behind the B node created for the first time. At the same time, the endIndex of the old node is moved to B, and the startIndex of the new node is moved 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 startIndex of the new node is moved to A. The startIndex and endIndex of the old node remain 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 after diff, and inserted behind the E node created last time. At the same time, the startIndex of the old node is moved to B, and the startIndex of the new node is moved to B

 

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

The sixth time, the startIndex of the new node is greater than the endIndex, and all nodes between newStartIdx and newEndIdx need to be created, that is, node F, and the real node corresponding to the F node is directly created and placed behind the B node

 

 

 

Principle analysis

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

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
        }
    }
}

 

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

★There is no new node, directly trigger the destroy hook of the old node
★There is no old node, it means that it is when the page is just initialized, at this time, there is no need to compare at all, it is all new, so only call createElm
★Old node and new node The same as itself, judge whether the nodes are the same through sameVnode, if they are the same, directly call patchVnode to process the two nodes The
old node and the new node are not the same, when the two nodes are different, directly create a new node and delete the old node

The following is mainly about the patchVnode 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)
    }
  }

patchVnode mainly made several judgments:

◆Whether the new node is a text node, if so, directly update the text content of the dom to the text content of the new node ◆
If both the new node and the old node have child nodes, then process the comparison and update the child nodes
◆Only the new node has child nodes, There is no old node, 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 DOMs and adding them to parent nodes Only old nodes have child nodes and new nodes do not
, Explain that all the old nodes are gone in the updated page, so what to do is to delete all the old nodes, that is, directly delete the DOM

If the child nodes are not exactly the same, call updateChildren

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]
      }
    }

The while loop mainly handles the following five scenarios:

★When the start of the new and old VNode nodes is the same, directly patchVnode, and at the same time, the start index of the new and old VNode nodes is increased by 1
. 1
★When the start of the old VNode is the same as the end of the new VNode, at this time after patchVnode, the current real dom node needs to be moved to the back of oldEndVnode, and at the same time, the start index of the old VNode is increased by 1, and the end of the new VNode is Index minus 1
★When the end of the old VNode node is the same as the start of the new VNode node, at this time after patchVnode, the current real dom node needs to be moved to the front of oldStartVnode, and the end index of the old VNode node is reduced by 1, and the new VNode node Add 1 to the start index of
★If none of the above four conditions are met, it means that there is no identical node that can be reused, and it will be divided into the following two situations:

1. Find the old VNode node with the same key as newStartVnode from the hash table with the old VNode as the key value and the corresponding index sequence as the value value, then perform patchVnode, and move the real dom to the front of the real dom corresponding to oldStartVnode
2. Call createElm to create a new dom node and place it at the current position of newStartIdx

Summarize


●When the data changes, the subscriber watcher will call patch to patch the real DOM
●Judging by isSameVnode, if they are the same, call the patchVnode method

patchVnode does the following:

1. Find the corresponding real dom, called el
2. If both have text nodes and are not equal, set the el text node as the text node of Vnode
3. If oldVnode has child nodes but VNode does not, delete the el child nodes
4. If oldVnode has no child nodes but VNode has them, then add the child nodes of VNode to el after realizing them.
5. If both have child nodes, execute the updateChildren function to compare child nodes
updateChildren mainly performs the following operations:

1. Set the head and tail pointers of the old and new VNode
2. Compare the old and new head and tail pointers, move closer to the middle in a loop, call patchVnode to repeat the patch process according to the situation, call createElem to create a new node, and find the VNode node with the same key from the hash table. Operate according to the situation
 

 

Guess you like

Origin blog.csdn.net/wenmin1987/article/details/129250001