Explore Vue diff algorithm

Posted by kevincompton on Wed, 15 Sep 2021 06:17:09 +0200

1. Virtual dom

fictitious DOM ( Virtual DOM )Yes DOM of JS Abstract representations, they are JS Object, able to describe DOM Structure and relationship. Various state changes of the application will act on the virtual DOM , finally mapped to DOM Come on.
advantage
1. Virtual DOM Lightweight and fast: when they change, through the old and new virtual DOM The comparison can get the minimum DOM Operating quantity, from
And improve performance
patch(vnode, h('div#app', obj.foo)) 

2. Cross platform: convert virtual dom updates to different runtime special operations to achieve cross platform

const patch = init([snabbdom_style.default])

patch(vnode, h('div#app', {style:{color:'red'}}, obj.foo))

three   Compatibility: you can also add compatibility codes to enhance the compatibility of operations  

1.1 comparison between real dom and virtual dom

Real dom

<div>
    <p>Hello World</p>
</div>

Virtual dom  

let vnode = {
    tag: 'div',
    children:[ {tag:'p', text:'Hello World'}]
}

2. diff algorithm

2.1 patch function

2.1.1 patch implementation

File   patch core\vdom\patch.js

First, perform tree level comparison. There may be three situations: add, delete and modify.
1. new VNode Delete if it does not exist;
2. old VNode If it does not exist, it will increase;
3. Implement if all exist diff Execute update

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // If the new virtual dom tree does not exist, it is deleted
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []

    //  If the old vdom does not exist, add a new one
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {

      // If the real node is passed in, the initialization operation is performed
      const isRealElement = isDef(oldVnode.nodeType)

      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // There are old and new vdom s
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // During initialization, create a new dom, append it to the body, and delete the array
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.

          // If there is a real node, there is the data server render attribute
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            // When the old Vnode is a rendering element on the server side, hydrating is marked as true
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
           // You need to map the virtual DOM to the real DOM with the hydrate function
          if (isTrue(hydrating)) {
            // It needs to be merged into the real DOM
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              // Call insert hook
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          
          // If it is not a server-side rendering element or merging to the real DOM fails, create an empty Vnode node to replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

3. patchVnode

Compare two VNode , including three types of operations: Attribute update, text update, child node update
The specific rules are as follows:
1. New and old nodes All children Child node, the child node is modified diffff Operation, calling updateChildren
2. If The old node has no child nodes, while the new node has child nodes , first clear the text content of the old node, and then add a new child node for it.
3. When The new node has no children, while the old node has children When, all child nodes of the node are removed.
4. When New and old nodes have no child nodes When, it's just a replacement of text.
let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    // Check whether the old and new nodes have child queues
    const oldCh = oldVnode.children
    const ch = vnode.children

    // Attribute update
    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)
    }

    // Judge whether it is an Element. If there is no text, it is an Element
    if (isUndef(vnode.text)) {

      // All have children
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // Only new nodes have children
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        // Empty old node text
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')

        // Add child
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // Only the old node has children
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // Old node has text
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // Both old and new are text and different
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

4. updateChildren

Here comes the point!!!!  

updateChildren The main function is to compare the old and the new in a more efficient way VNode of children Get the minimum operation patch. Executing a double loop is the traditional way, vue Medium for web Special algorithm optimization has been made for the scene features. Let's look at the picture:

 

There are four situations:

1. The old node is the same as the new header node, and directly patchVnode

2. The old tail node is the same as the new tail node, directly patchVnode

3. The old node is the same as the new tail node. While patchVnode, the old node should be placed behind the old tail node

4. The old tail node is the same as the new head node. While patchVnode, place the old tail node in front of the old node

If none of the above four conditions are met:

In the old node queue, look for the node that satisfies sameVnode with the new head node, and then move it to the front of the old node, and the subscript of the new head node moves back one bit

Of course, if such a node cannot be found, create a new node and put it at the front of the old node queue

Finally, the queue lengths of the old node and the new node may be different, and there will be one queue that has not been traversed, so there will be the following two cases:

Batch addition (after traversing the old nodes, new nodes that have not been traversed will be created and added in batch)

Batch deletion (after traversing the new nodes, delete the old nodes that have not been traversed in batch)

Direct source code

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

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

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    /**
     * Both sides close to the middle, dfs
     */
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // The old start node does not exist. Move the subscript one bit back
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      } else if (isUndef(oldEndVnode)) {
        // The old tail node does not exist. The subscript moves forward one bit
        oldEndVnode = oldCh[--oldEndIdx]

      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // The old and new start nodes are the same and + 1 at the same time
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // The old and new end nodes are the same, and - 1
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // The old head node is the same as the new tail node. Move the old head node to the old tail to improve the similarity
        // 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)) { 
        // The old tail node is the same as the new head node
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      } else {
        // The four conjectures did not find the same, so they were forced to search circularly

        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        
        // Find the index key in the old array
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // If there is no such element in the old child node array, a new one will be created
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {

          // If the element is found to be reusable, further comparison is made
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // Is exactly the same element
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // Only the key is the same, but the content is different
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }

    // Finally, finish the work
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm

      // Batch creation
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {

      // Batch delete
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

 

Topics: Javascript Front-end