[VUE]—Principle of diff algorithm

Series Article Directory

[VUE] — Principle of watch listener

1. Introduction

In vuewill maintain DOM an vnodeobject corresponding to the node.

vnode The object of the corresponding child node in the children array vnode, so in vue the map by vnode and the real DOM tree , we also call it a virtual tree.

It is with the virtual tree, when the data is updated. We can compare the differences between the new data and vnode the old data to achieve accurate updates.oldVnode

And our algorithm to compare the difference is used diff. By diffcomparing the difference of the virtual tree, the difference patchis updated to the corresponding real DOM node .

2. Source code analysis

2.1 patch function

patchThe function is the entry function of the diff process . First of all, we need to know that it patchwill be loaded once when the page is rendered , and then vnodecalled when it changes.

// 首次渲染是DOM元素,后面是vnode
function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    
    
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

	// 不是一个vnode就创建一个空的vnode并关联在DOM元素上
    if (!isVnode(oldVnode)) {
    
    
      // 创建一个空的vnode,并关联DOM元素
      oldVnode = emptyNodeAt(oldVnode)
    }

    if (sameVnode(oldVnode, vnode)) {
    
    
      // key、tag相同,说明是同一个vnode
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
    
    
      // key、tag不相同,说明是不同的vnode
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
      
      // 创建新的DOM元素
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
    
    
        // 插入新的DOM元素
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除老的DOM元素
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
    return vnode
  }

The whole logic is:
if both nodes are the same, then check their children in depth. If the two nodes are not the same, it means that vnodethey have been completely changed and can be replaced directly oldVnode.

2.2 sameVnode

// 判断新旧节点是否一样
function sameVnode (a, b) {
    
    
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

2.3 patchVnode

If the two nodes are the same, it will enter patchVnodethe method to judge its child nodes and text nodes. This is to patch reusable nodes, that is, to distribute updates.

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    
    
   	...
    // vonde为新的vnode oldvnnode为老的vnode
    // 设置vnode关联的DOM元素 
    const elm = vnode.elm = oldVnode.elm!

    // 老children
    const oldCh = oldVnode.children as VNode[]
    
    // 新children
    const ch = vnode.children as VNode[]

    if (oldVnode === vnode) return
	...
    // 新vnode 无text 有children
    if (isUndef(vnode.text)) {
    
    
      // 新vnode 有children 老vnode 有chidren
      if (isDef(oldCh) && isDef(ch)) {
    
    
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      // 新vnode 有children 旧vnode 无children 有text
      } else if (isDef(ch)) {
    
    
        // 清空text
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 添加children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        
      // 新vnode 无children 旧vnode 有children
      } else if (isDef(oldCh)) {
    
    
        // 移除children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        
      // 老vnode 有text
      } else if (isDef(oldVnode.text)) {
    
    
        // 清空text
        api.setTextContent(elm, '')
      }
    // 新vnode 有text 无children
    // 老vnode text 不等于 新vnode text
    } else if (oldVnode.text !== vnode.text) {
    
    
      // 老vnode 有children
      if (isDef(oldCh)) {
    
    
        // 移除老vnode children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 设置新vnode text
      api.setTextContent(elm, vnode.text!)
    }
    hook?.postpatch?.(oldVnode, vnode)
  }

patchVnodeThe logic of the function:

  1. Find the corresponding DOMnode elmand assign it to the new onevnode.elm
  2. Determine the new node type (vnode.text), if it is a text node , just update the elmtext
  3. Under non-text nodes , determine the child nodes of the old and new nodes
  4. If the old and new nodes have child nodes , go through the same layer comparison process of the child nodesupdateChildren
  5. If only the new node has children , use directly addVnodesto elmadd child nodes (delete the text first)
  6. If only the old node has child nodes , use to removeVnodesremove
  7. If there is no child node , judge whether the old data has a text node, and clear it.

2.4 updateChildren

 function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    
    
    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: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any
​
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
      if (oldStartVnode == null) {
    
    
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
    
    
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
    
    
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
    
    
        newEndVnode = newCh[--newEndIdx]// 1.老开始和新开始对比
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]// 2.老结束和新结束对比
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]// 3.老开始和新结束对比
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
    
     // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]// 4.老结束和新开始对比
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
    
     // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]// 以上四种情况都没有命中
      } else {
    
    
        if (oldKeyToIdx === undefined) {
    
    
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }// 拿到新开始的key,在老children里去找有没有某个节点有对应这个key
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 没有在老children里找到对应的
        if (isUndef(idxInOld)) {
    
     // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)// 在老children里找到了对应的
        } else {
    
    
          // 找到了对应key的元素(key相等)
          elmToMove = oldCh[idxInOld]// key相等 判断tag是否相等
          if (elmToMove.sel !== newStartVnode.sel) {
    
    
            // key相等 tag不相等
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
    
    
            // key相等 tag相等
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    //当节点遍历完之后,会存在两种情况,“新数组已经遍历完,但旧数组没有遍历完成” 和 “旧数组遍历完成,但新数组没有遍历完成”。故源代码的判断如下:
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    
    
      if (oldStartIdx > oldEndIdx) {
    
    
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
    
    
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }

analyze:
insert image description here

  • Step 1: Determine whether oldStartVnode, newStartVnodethe two pointers vnodeare matched. If they match, the pointeroldStartVnode,newStartVnode moves to the right, and the position of the real DOM remains unchanged.
  • Step 2: oldEndVnode, newEndVnodeWhether they match, if they match, the pointeroldEndVnode, newEndVnode moves to the left, and the position of the real DOM remains unchanged.
  • Step 3: oldStartVnode, newEndVnodeDetermine whether it matches, if it matches, the first node in the real DOM will be moved to the end
  • Step 4: oldEndVnode, newStartVnodeJudging that the match is matched. If it matches, the last node in the real DOM will move to the front, and the two pointers on the match will move to the middle.
  • Step 5: If none of the four matches is successful, there are two cases
    • If both old and new child nodes exist key, a table will be generated according to the corresponding table, and it will be used to oldChildmatch the table . If the match is successful, it will be judged whether the matching node is or not . If it is, the successful node will be moved to the front in the real DOM. Otherwise, the generated corresponding node will be inserted into the corresponding position in , the pointer will move to the middle, and the matching node will be set to .keyhashnewStartVnodekeyhashnewStartVnodesameNodenewStartVnodeDOMnewStartVnodenewStartVnodeoldnull
    • If not key, directly insert the newStartVnodenew node into the real one DOM(ps: this can explain why v-forit needs to be set key, if not, keythen only four kinds of matching will be done, even if there are reusable nodes in the middle of the pointer, they cannot be duplicated. used)

3. Optimization of diff algorithm in vue3

Vue3.0 is filtered for "brainless" patchVnode- static type Vnode:
old version of the source code:
insert image description here
Here, we will repeat the comparison and update logic of the vue2.x series:
insert image description here
the new version has been vue3added 静态类型Vnode, if 静态类型的vnodeso, skip the update directly and modify the new one Node reference is enough:
insert image description here
Remarks: commentThe type is currently turned to its source code and only changes the reference, and the source code author adds a line of comment.
insert image description here
Here is another sentence, the flagment fragment type is the newly added vnode type, namely:
insert image description here

Guess you like

Origin blog.csdn.net/weixin_44761091/article/details/124141686