Explain the virtual DOM and Diff algorithm in simple terms, and the differences between Vue2 and Vue3

Keywords: Javascript Vue Vue.js Algorithm source code

Because the Diff algorithm calculates the difference of virtual DOM, first pave a little bit of virtual DOM, understand its structure, and then uncover the veil of Diff algorithm layer by layer, in simple terms, to help you thoroughly understand the principle of Diff algorithm

Understanding virtual DOM

Virtual DOM is simply to use JS objects to simulate DOM structure

How does it use JS objects to simulate DOM structure? Look at an example

<template>
    <div id="app" class="container">
        <h1>Mu Hua</h1>
    </div>
</template>

The above template is transferred to the virtual DOM, which is the following

{
  'div',
  props:{ id:'app', class:'container' },
  children: [
    { tag: 'h1', children:'Mu Hua' }
  ]
}

Such a DOM structure is called virtual DOM (Virtual Node), or vnode for short.

Its expression is to turn each tag into an object, which can have three attributes: tag, props and children

  • tag: required. It's the label. It can also be a component or a function
  • props: not required. These are the properties and methods on this label
  • Children: not required. It is the content or child node of the label. If it is a text node, it is a string. If there is a child node, it is an array. In other words, if you judge that children is a string, it means that it must be a text node, and this node must have no child elements

Why use virtual DOM? Look at a picture

As shown in the figure, the native DOM has many properties and events, and even creating an empty div costs a lot. The point of using virtual DOM to improve performance is to calculate the DOM to be changed by comparing the diff algorithm with the DOM before data change, and then only operate on the changed DOM instead of updating the whole view

How to convert DOM into virtual DOM in Vue? If you are interested, you can follow my another article to learn more about the template compilation process and principle in Vue

In Vue, the data update mechanism of virtual DOM adopts asynchronous update queue, which is to load the changed data into an asynchronous queue of data update, that is, patch, which is used to compare new and old vnode s

Cognitive Diff algorithm

Diff algorithm is called patch in Vue, and its core is reference Snabbdom , through the comparison between the old and new virtual DOM (i.e. patch process), find out the place with the smallest change and switch to DOM operation

extend
In Vue1, there is no patch. Each dependency has a separate Watcher to update. When the project scale becomes larger, the performance can't keep up. Therefore, in Vue2, in order to improve the performance, there is only one Watcher for each component. How can we accurately find the location of the change in the component when we need to update? So patch it's coming

So when did it execute?

When the page is rendered for the first time, the patch will be called and a new vnode will be created without deeper comparison

Then, when the data in the component changes, the setter will be triggered, and then Notify the watcher through Notify. The corresponding Watcher will Notify the update and execute the update function. It will execute the render function to obtain the new virtual DOM, and then execute the patch to compare the old virtual dom of the last rendering result and calculate the minimum change, Then update the real DOM, that is, the view, according to this minimum change

So how is it calculated? Look at a picture first

For example, with a DOM structure like the one shown above, how do you calculate the change? Simply put

  • Traversing the old virtual DOM
  • Traverse the new virtual DOM
  • Then reorder according to changes, such as changes and additions above

But this will have a big problem. If there are 1000 nodes, you need to calculate 1000 ³ Times, that is, 1 billion times, which is unacceptable. Therefore, when using the Diff algorithm in Vue or React, it follows the depth first strategy and makes some optimization based on the same layer comparison strategy to calculate the minimum change

Optimization of Diff algorithm

1. Compare only the same level, not cross level

As shown in the figure, the Diff process will only compare the DOM of the same level framed with the same color, so as to simplify the comparison times. This is the first aspect

2. Compare tag names

If the comparison tag names of the same level are different, the nodes corresponding to the old virtual DOM will be removed directly, and the depth comparison will not continue according to the tree structure. This is the second aspect of simplifying the number of comparisons

3. Compare key

If the tag names are the same and the keys are the same, it will be considered as the same node, and we will not continue to make depth comparison according to this tree structure. For example, when we write v-for, we will compare the keys, and if we do not write the key, an error will be reported, which is because the Diff algorithm needs to compare the keys

There is a very common question in the interview, which is to let you talk about the role of key. In fact, it tests everyone's mastery of the details of virtual DOM and patch, which can reflect the understanding level of our interviewers, so here's an extension of key

Role of key

For example, there is a list. We need to insert an element in the middle. What will happen? Look at a picture first

As shown in the figure, li1 and li2 will not be re rendered, which is not controversial. li3, li4 and li5 will be re rendered

When we do not use the key or the index of the list as the key, the corresponding location relationship of each element is index. The results in the above figure directly lead to the change of the corresponding location relationship from the inserted element to all the subsequent elements, so all the elements will be updated, which is not what we want, What we want is to render the added element, and do not re render the other four elements without making any changes

In the case of using a unique key, the location relationship of each element is the key. Let's take a look at the case of using a unique key value

In this way, li3 and li4 in the figure will not be re rendered, because the element content has not changed and the corresponding positional relationship has not changed.

This is why v-for must write a key, and it is not recommended to use the index of the array as the key in development

To sum up:

  • key is mainly used to update the virtual DOM more efficiently, because it can find the same node very accurately, so the patch process will be very efficient
  • When Vue judges whether two nodes are the same during the patch process, key is a necessary condition. For example, if the key is not written when rendering the list, Vue may update elements frequently during comparison, making the whole patch process inefficient and affecting performance
  • We should avoid using the array subscript as the key, because if the key value is not unique, it may lead to the bug shown in the figure above, making Vue unable to distinguish it from others. For example, when using the same label element for transition switching, it will only replace its internal attributes without triggering the transition effect
  • It can be seen from the source code that Vue judges whether the two nodes are the same, mainly judging their element types and keys. If you do not set the key, you may always think that the two nodes are the same and can only do update operations, resulting in a large number of unnecessary DOM update operations, which is obviously undesirable

If you are interested, you can take a look at the source code: SRC \ core \ vdom \ patch.js - line 35 sameVnode(), which is also described in detail below

Diff algorithm core principle - source code

The Diff algorithm is mentioned above. In Vue, it is a patch, which paves the way. Let's go to the source code to see what this amazing patch has done?

patch

Source address: Src / core / vdom / patch.js - line 700

In fact, patch is a function. Let's first introduce the core process in the source code, and then take a look at the source code of patch. There are comments on each line in the source code

It can receive four parameters, mainly the first two

  • oldVnode: old virtual DOM node
  • vnode: new virtual DOM node
  • hydrating: is it to be mixed with the real DOM? It will be used for server-side rendering. I won't explain it here
  • removeOnly: transition group will be used. I won't explain it here

The main process is as follows:

  • If vnode does not exist and oldVnode exists, delete oldVnode
  • Vnode exists. If oldVnode does not exist, create vnode
  • If both exist, compare whether they are the same node through the sameVnode function (detailed explanation later)
    • If it is the same node, subsequent comparison of node text changes or child node changes can be made through patchVnode
    • If it is not the same node, mount vnode under the parent element of oldVnode
      • If the root node of the component is replaced, the parent node is traversed and updated, and then the old node is deleted
      • If it is a server-side rendering, mix oldVnode with the real DOM with hydrating

Let's look at the complete patch function source code, which shows that I have written it in the comments

// Two judgment functions
function isUndef (v: any): boolean %checks {
  return v === undefined || v === null
}
function isDef (v: any): boolean %checks {
  return v !== undefined && v !== null
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
    // If the new vnode does not exist, but the oldVnode does exist
    if (isUndef(vnode)) {
      // If oldVnode exists, call the component unloading hook destroy of oldVnode
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
    
    // If oldVnode does not exist, the new vnode must exist, such as when rendering for the first time
    if (isUndef(oldVnode)) {
      isInitialPatch = true
      // Create a new vnode
      createElm(vnode, insertedVnodeQueue)
    } else {
      // The rest are new vnodes and oldvnodes
      
      // Is it an element node
      const isRealElement = isDef(oldVnode.nodeType)
      // Is the element node & & compare the same node through sameVnode (see details later in the function)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // If yes, use patchVnode for subsequent comparison (detailed explanation is given later in the function)
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // If it's not the same element node
        if (isRealElement) {
          // const SSR_ATTR = 'data-server-rendered'
          // If it is an element node and has the attribute 'data server rendered'
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            // It is rendered by the server. Delete this attribute
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          // In this judgment, the processing logic of server-side rendering is mixing
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn('This is a long warning message')
            }
          }
          // function emptyNodeAt (elm) {
          //    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
          //  }
          // If it is not rendered by the server, or the mixing fails, create an empty annotation node to replace oldVnode
          oldVnode = emptyNodeAt(oldVnode)
        }
        
        // Get the parent node of oldVnode
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        
        // Create a DOM node based on the new vnode and mount it on the parent node
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        
        // If the root node of the new vnode exists, that is, the root node has been modified, you need to traverse and update the parent node
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          // Recursively update the elements under the parent node
          while (ancestor) {
            // Uninstall all components under the old root node
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            // Replace existing element
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            // Update parent node
            ancestor = ancestor.parent
          }
        }
        // If the old node still exists, delete the old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          // Otherwise, uninstall oldVnode directly
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // Returns the updated node
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

sameVnode

Source address: Src / core / vdom / patch.js - line 35

This is a function used to determine whether it is the same node

This function is not long. Just look at the source code

function sameVnode (a, b) {
  return (
    a.key === b.key &&  // Is key the same
    a.asyncFactory === b.asyncFactory && ( // Is it an asynchronous component
      (
        a.tag === b.tag && // Is the label the same
        a.isComment === b.isComment && // Is it a comment node
        isDef(a.data) === isDef(b.data) && // Is the content data the same
        sameInputType(a, b) // Determine whether the input type is the same
      ) || (
        isTrue(a.isAsyncPlaceholder) && // Determine whether the placeholder for distinguishing asynchronous components exists
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

patchVnode

Source address: Src / core / vdom / patch.js - line 501

This function is only executed when the new vnode and oldVnode are the same node. It is mainly used to compare the changes of node text or child nodes

Let's first introduce the main process and then look at the source code. The process is as follows:

  • If the reference addresses of oldVnode and vnode are the same, it means that the node has not changed and is returned directly
  • If the isAsyncPlaceholder of oldVnode exists, it skips the check of asynchronous components and returns directly
  • If both oldVnode and vnode are static nodes with the same key, and vnode is a clone node or a node controlled by the v-once instruction, copy oldVnode.elm and oldVnode.child to vnode, and then return
  • If vnode is neither a text node nor a comment
    • If both vnode and oldVnode have child nodes and the child nodes are different, call updateChildren to update the child nodes
    • If only vnode has child nodes, addVnodes is called to create child nodes
    • If only oldVnode has a child node, removeVnodes is called to delete the child node
    • If the vnode text is undefined, delete the vnode.elm text
  • If vnode is a text node but the text content is different from oldVnode, the text is updated
  function patchVnode (
    oldVnode, // Old virtual DOM node
    vnode, // New virtual DOM node
    insertedVnodeQueue, // Queue to insert node
    ownerArray, // Node array
    index, // Subscript of current node
    removeOnly // Only in
  ) {
    // The reference addresses of new and old nodes are the same, and they are returned directly
    // For example, when props is not changed, the sub components are not rendered and reused directly
    if (oldVnode === vnode) return
    
    // The new vnode is a real DOM element
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm
    // If the current node is annotated or v-if, or is an asynchronous function, skip checking asynchronous components
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // When the current node is a static node, the key is the same, or when there is v-once, it is directly assigned and returned
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    // Don't worry about hook related
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    // Get a list of child elements
    const oldCh = oldVnode.children
    const ch = vnode.children
    
    if (isDef(data) && isPatchable(vnode)) {
      // Traverse and call update to update all properties of oldVnode, such as class,style,attrs,domProps,events
      // The update hook function here is the hook function of vnode itself
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // The update hook function here is the function we passed
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // If the new node is not a text node, that is, it has child nodes
    if (isUndef(vnode.text)) {
      // If both old and new nodes have child nodes
      if (isDef(oldCh) && isDef(ch)) {
        // If the child nodes of the new and old nodes are different, execute the updateChildren function to compare the child nodes
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // If the new node has child nodes, it means that the old node has no child nodes
        
        // If the old node is a text node, that is, there is no child node, it will be cleared
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // Add child node
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // If the new node has no child nodes and the old node has child nodes, it will be deleted
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // If the old node is a text node, it is cleared
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // If the new and old nodes are text nodes and the text is different, update the text
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // Execute postpatch hook
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren

Source address: Src / core / vdom / patch.js - line 404

This is the function of comparing child nodes when the new vnode and oldVnode have child nodes and the child nodes are different

Here is the key, the key!

For example, there are two child node lists for comparison. The main comparison process is as follows

Loop through two lists. The loop stop condition is that the start pointer startIdx and end pointer endIdx of one list coincide

The contents of the cycle are:{

  • The new head contrasts with the old one
  • The new tail contrasts with the old one
  • The new head contrasts with the old tail
  • The new tail is compared with the old head. These four comparisons are shown in the figure

As long as one of the above four judgments is equal, call patchVnode to compare the changes of node text or child nodes, and then move the subscript of the comparison to continue the next round of circular comparison

If the above four situations fail to hit, keep taking the key of the new start node to the old children

  • If not, create a new node
  • If found, compare whether the label is the same node
    • If it is the same node, call patchVnode for subsequent comparison, then insert this node in front of the old start, and move the new start subscript to continue the next round of circular comparison
    • If it is not the same node, a new node is created
      }
  • If the old vnode is traversed first, add the nodes that the new vnode has not traversed
  • If the new vnode is traversed first, delete the nodes that the old vnode has not traversed

Why is there head to tail and tail to head operation?

Because it can quickly detect the reverse operation and speed up the Diff efficiency

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // Subscript of old vnode traversal
    let newStartIdx = 0 // Subscript of new vnode traversal
    let oldEndIdx = oldCh.length - 1 // Length of old vnode list
    let oldStartVnode = oldCh[0] // The first child element of the old vnode list
    let oldEndVnode = oldCh[oldEndIdx] // The last child element of the old vnode list
    let newEndIdx = newCh.length - 1 // New vnode list length
    let newStartVnode = newCh[0] // The first child element of the new vnode list
    let newEndVnode = newCh[newEndIdx] // The last child element of the new vnode list
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    const canMove = !removeOnly
    
    // Loop. The rule is that the start pointer moves to the right and the end pointer moves to the left
    // The loop ends when the start and end pointers coincide
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
        
        // The old start contrasts with the new start
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // It is a recursive call from the same node to continue to compare the contents and child nodes of the two nodes
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // Then move the pointer back one bit and compare it from front to back
        // For example, compare [0] of two lists for the first time, and then compare [1]..., which is the same later
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
        
        // Comparison between old end and new end
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // Then move the pointer forward one bit and compare it from back to front
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
        
        // Contrast the old beginning with the new end
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // The old list takes values from front to back, and the new list takes values from back to front, and then compares them
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
        
        // The old end contrasts with the new beginning
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // The old list takes values from back to front, and the new list takes values from front to back, and then compares them
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
        
        // The above four situations are missed
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // Get the new key and find out whether a node has this key in the old children
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
          
        // There are in the new children, but the corresponding elements are not found in the old children
        if (isUndef(idxInOld)) {
          ///Create a new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // The corresponding element was found in the old children
          vnodeToMove = oldCh[idxInOld]
          // Judge if the label is the same
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // Update the two same nodes
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // If the label is different, create a new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // Oldstartidx > oldendidx indicates that the old vnode is traversed first
    if (oldStartIdx > oldEndIdx) {
      // Add the node from newStartIdx to newEndIdx
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    
    // Otherwise, it means that the new vnode is traversed first
    } else if (newStartIdx > newEndIdx) {
      // Delete the nodes that are not traversed in the old vnode
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

So far, the core logic source code of the whole Diff process is over. Let's take a look at the changes made in Vue 3

Vue3 optimization

The source code version of this article is Vue2. The Diff algorithm is rewritten in Vue3. Therefore, the source code is basically different, but the things to be done are the same

The full Diff source code analysis of Vue3 is still being written and will be released in a few days. First, let's introduce the optimization compared with Vue2. The data released by Youda is that the update performance has been improved by 1.3 ~ 2 times and the ssr performance has been improved by 2 ~ 3 times. Let's see what optimizations are available

  • Event caching: event caching can be understood as static
  • Add static tag: Vue2 is full Diff, Vue3 is static tag + partial Diff
  • Static promotion: saved when a static node is created, and then reused directly
  • The longest increment subsequence is used to optimize the comparison process: in Vue2, the changes are compared in the updateChildren() function, and in Vue3, the logic of this block is mainly in the patchKeyedChildren() function. See the following for details

Event cache

For example, a button with a click event

<button @click="handleClick">Button</button>

Let's look at the results after Vue3 is compiled

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
  }, "Button"))
}

Note that onClick will read the cache first. If there is no cache, it will store the incoming events in the cache, which can be understood as becoming a static node. Excellent, but there is no cache in Vue2, which is dynamic

Static tag

What are static tags?

Source address: packages/shared/src/patchFlags.ts

export const enum PatchFlags {
  TEXT = 1 ,  // Dynamic text node
  CLASS = 1 << 1,  // 2 dynamic class
  STYLE = 1 << 2,  // 4 dynamic style
  PROPS = 1 << 3,  // 8 dynamic attributes other than class/style
  FULL_PROPS = 1 << 4,       // 16 for nodes with dynamic key attribute, when the key is changed, a complete diff comparison is required
  HYDRATE_EVENTS = 1 << 5,   // 32 nodes with listening events
  STABLE_FRAGMENT = 1 << 6,  // 64 a fragment that does not change the order of child nodes (multiple root elements in a component will be wrapped with fragments)
  KEYED_FRAGMENT = 1 << 7,   // 128 fragment s with key attribute or some child nodes have keys
  UNKEYEN_FRAGMENT = 1 << 8, // 256 child nodes do not have fragment s of key s
  NEED_PATCH = 1 << 9,       // 512 only non props comparison will be performed for one node
  DYNAMIC_SLOTS = 1 << 10,   // 1024 dynamic slot
  HOISTED = -1,  // Static node 
  BAIL = -2      // Indicates that the Diff process does not require optimization
}

What's the use of static tags? Look at a picture

Where is it used? For example, the following code

<div id="app">
    <div>Mu Hua</div>
    <p>{{ age }}</p>
</div>

As a result of compiling in Vue2, those interested can install Vue template compiler and test by themselves

with(this){
    return _c(
      'div',
      {attrs:{"id":"app"}},
      [ 
        _c('div',[_v("Mu Hua")]),
        _c('p',[_v(_s(age))])
      ]
    )
}

The results compiled in Vue3 are as follows. Those interested can click here Self test

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "Mu Hua", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
  ]))
}

See - 1 and 1 in the above compilation results? These are static tags, which are not available in Vue2. This tag will be judged in the patch process to Diff optimize the process and skip some static node comparison

Static lift

In fact, take the example of Vue2 and Vue3 static tags above. In Vue2, whenever an update is triggered, no matter whether the element participates in the update or not, it will be recreated every time, which is the following pile

with(this){
    return _c(
      'div',
      {attrs:{"id":"app"}},
      [ 
        _c('div',[_v("Mu Hua")]),
        _c('p',[_v(_s(age))])
      ]
    )
}

In Vue3, the element that does not participate in the update will be saved, created only once, and then reused every time it is rendered. For example, in the above example, it will be created and saved statically

const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "Mu Hua", -1 /* HOISTED */)

Then, each time the age is updated, only the dynamic content is created and the static content saved above is reused

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _hoisted_2,
    _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
  ]))
}

patchKeyedChildren

In Vue2, updateChildren will update

  • Head to head ratio
  • Tail and tail ratio
  • Head to tail ratio
  • Tail to head ratio
  • All missed

In Vue3, patchKeyedChildren is

  • Head to head ratio
  • Tail and tail ratio
  • Move / add / delete based on the longest increasing subsequence

Take an example, for example

  • Old children: [a, b, c, d, e, f, g]
  • New children: [a, b, f, c, d, e, h, g]
  1. First, compare the head to the head. If you find a difference, end the cycle and get [a, b]
  2. Then the tail and tail ratio are carried out. If it is found that there is a difference, the cycle is ended and [g] is obtained
  3. Save the node [f, c, d, e, h] that has not been compared, and get the corresponding subscript in the array through newIndexToOldIndexMap to generate the array [5, 2, 3, 4, - 1]. If - 1 is not in the old array, it means it is new
  4. Then take out the longest increasing subsequence in the array, that is, the node [c, d, e] corresponding to [2, 3, 4]
  5. Then, you only need to move / add / delete other remaining nodes based on the location of [c, d, e]

Using the longest increment subsequence can minimize the movement of DOM and achieve the least DOM operations. If you are interested, go to leet code question 300 (longest increment subsequence) to experience

Previous highlights

epilogue

If this article is of little help to you, please give me a praise and support. Thank you

Posted by tomato on Wed, 13 Oct 2021 08:51:49 -0700