preface

In vue.js, component is a very important concept, the whole application page is realized by component rendering, this paper mainly analyzes how to update the internal component, which also involves the core DOM Diff algorithm.

Side effect rendering function

In simple terms, a reactive data change in a function triggers the execution of the function. In Vue, a data change triggers the setupRenderEffect side effect rendering function to update the component.

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) = > {
  // Create a reactive side effect rendering function
  instance.update = effect(function componentEffect() {
    if(! instance.isMounted) {// Render component
    }
    else {

      // Update the component
      let { next, vnode } = instance
      // next represents the new component vNode
      if (next) {
        // Update component vNode information
        updateComponentPreRender(instance, next, optimized)
      }
      else {
        next = vnode
      }
      // Render a new subtree vnode
      const nextTree = renderComponentRoot(instance)
      // Cache the old subtree vnode
      const prevTree = instance.subTree
      // Update the subtree vnode
      instance.subTree = nextTree
      // The component updates the core logic and makes patches according to the old and new subtree vNodes
      patch(prevTree, nextTree,
        // The parent may have changed in the teleport component, so the container looks directly for the parent of the old tree DOM element
        hostParentNode(prevTree.el),
        // Reference nodes in fragments can change, so look directly for the next node of the old tree DOM element
        getNextHostNode(prevTree),
        instance,
        parentSuspense,
        isSVG)
      // Cache the updated DOM node
      next.el = nextTree.el
    }
  }, prodEffectOptions)
}
Copy the code

As you can see, updating a component does three main things:

  • Update component vNode node: There is a condition to determine whether there is a new component vnode in the component instance (represented by Next), if there is an update component vnode, and if there is no next pointing to the previous component vnode.
  • Render new subtree VNodes: Because the data has changed and the template is related to the data, the resulting subtree VNodes will change accordingly.
  • Execute patch logic according to the old and new subtree vnodes: render the new subtree vnode, because the data has changed, and the template is related to the data, so the generated subtree vnode will also change accordingly, next we will analyze this process.

Patch process

Implementation code of patch process:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) = > {
  // If there are old and new nodes, and the types of the old and new nodes are different, the old node is destroyed
  if(n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense,true)
    // Set n1 to null to ensure that the subsequent mount logic is used
    n1 = null
  }
  const { type, shapeFlag } = n2
  switch (type) {
    case Text:
      // Process text nodes
      break
    case Comment:
      // Process the comment node
      break
    case Static:
      // Handle static nodes
      break
    case Fragment:
      // Process the Fragment element
      break
    default:
      if (shapeFlag & 1 /* ELEMENT */) {
        // Handle normal DOM elements
        processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 6 /* COMPONENT */) {
        // Process components
        processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else if (shapeFlag & 64 /* TELEPORT */) {
        / / processing TELEPORT
      }
      else if (shapeFlag & 128 /* SUSPENSE */) {
        / / deal with SUSPENSE}}}function isSameVNodeType (n1, n2) {
  // The type and key of n1 and n2 nodes are the same
  return n1.type === n2.type && n1.key === n2.key
}
Copy the code

In this process, first check whether the old and new nodes are of the same vNode type. If they are different, for example, if a DIV is updated to a UL, the simplest operation is to delete the old DIV node and then mount the new UL node.

If it is the same VNode type, the diff update process will be performed, and then different processing logic will be performed depending on the vNode type. Here we will still only analyze the processing for common element types and component types.

Processing components

Logic for component updates:

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  if (n1 == null) {
    // Mount the component
  }
  else {
    // Update child components
    updateComponent(n1, n2, parentComponent, optimized)
  }
}
const updateComponent = (n1, n2, parentComponent, optimized) = > {
  const instance = (n2.component = n1.component)
  // Determine whether a child component needs to be updated based on the old and new child vNodes
  if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
    // Assign the new child vnode to instance.next
    instance.next = n2
    // Child components may also be added to the update queue due to data changes. Removing them prevents repeated updates to a child component
    invalidateJob(instance.update)
    // Execute the side effect rendering function of the child component
    instance.update()
  }
  else {
    // No update is required, only properties are copied
    n2.component = n1.component
    n2.el = n1.el
  }
}
Copy the code

ProcessComponent updates the child component by executing the updateComponent function, which shouldUpdateComponent when updating the child component, Determine whether a child component needs to be updated based on the old and new child component vNode. All you need to know is that inside the shouldUpdateComponent function, it checks and compares the props, chidren, dirs, transiton and other properties of the vNode component to determine whether or not the child component needs to be updated.

And then we look at the updateComponent function, and if shouldUpdateComponent returns true, then at the end of it, InvalidateJob (instance.update) is executed to avoid repeated updates of the child component due to its own data changes, and instance.update, the side effect rendering function of the child component, is executed to trigger the update of the child component.

So processComponent handles the component vNode, essentially determining whether the child needs to be updated, recursively executing the child’s side effects rendering function to update it if necessary, otherwise just updating some vNode properties and letting the child component instances keep references to the component VNode. A new component vNode can be retrieved from within the render function when the child component’s data changes and the component is re-rendered.

Components are abstract, and updates to components ultimately fall into updates to normal DOM elements. So let’s take a closer look at how common elements are handled in component updates.

Working with common elements

Code implementation:

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  isSVG = isSVG || n2.type === 'svg'
  if (n1 == null) {
    // Mount the element
  }
  else {
    // Update the element
    patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
  }
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) = > {
  const el = (n2.el = n1.el)
  const oldProps = (n1 && n1.props) || EMPTY_OBJ
  const newProps = n2.props || EMPTY_OBJ
  / / update the props
  patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
  constareChildrenSVG = isSVG && n2.type ! = ='foreignObject'
  // Update the child node
  patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}
Copy the code

The process of updating an element does two things: update props and update child nodes.

This function is used to update the class, style, event, and other DOM properties of the DOM node.

Second, update the child node. Let’s look at the implementation of patchChildren function here:

const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) = > {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children
  const { shapeFlag } = n2
  // There are three possible cases of child nodes: text, array, and null
  if (shapeFlag & 8 /* TEXT_CHILDREN */) {
    if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
      // Array -> text, then delete the previous child node
      unmountChildren(c1, parentComponent, parentSuspense)
    }
    if(c2 ! == c1) {// If the text comparison is different, replace it with the new text
      hostSetElementText(container, c2)
    }
  }
  else {
    if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
      // The previous child node is an array
      if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        // The new child node is still an array, then the whole diff is done
        patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
      else {
        // Array -> empty, delete only the previous child node
        unmountChildren(c1, parentComponent, parentSuspense, true)}}else {
      // The previous child node is a text node or is empty
      // The new child node is an array or empty
      if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
        // If the previous child node was text, clear it
        hostSetElementText(container, ' ')}if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        // If the new child is an array, mount the new child
        mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
  }
}
Copy the code

There are three possible cases for vNodes, children of an element: plain text, vNode arrays, and null. So in terms of permutations and combinations there are nine cases for old and new child nodes. Let’s first look at the case where the old child node is plain text:

  • If the new child node is also plain text, do a simple text substitution;
  • If the new child node is empty, delete the old child node.
  • If the new child node is a vNode array, empty the text of the old child node first, and then add new children to the parent container of the old child node.

Let’s see if the old child node is empty:

  • If the new child node is plain text, add the new text node under the parent container of the old child node.
  • If the new child node is also empty, nothing needs to be done;
  • If the new child node is a vNode array, add more children to the parent container of the old child node.

Finally, let’s look at the case where the old child node is a vNode array:

  • If the new child node is plain text, delete the old child node first, and then add the new text node under the parent container of the old child node.
  • If the new child node is empty, delete the old child node.
  • If the new child node is also a VNode array, then you need to do the full diff of the old and new child nodes, which is the most complicated case, using the core diff algorithm internally.

The Diff algorithm

The change of the new child node array relative to the old child node array is accomplished by updating, deleting, adding and moving nodes, while the core diff algorithm aims to complete the update of the child node at a low cost under the condition that the DOM structure of the old child node, the Vnode and the Vnode of the new child node are known. Solve for a series of operations that generate a new child node DOM.

1. Synchronize the header node

Header node synchronization implementation code:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1
  // 1. Synchronize from the header
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      // Perform patch update recursively for the same node
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    else {
      break
    }
    i++
  }
}
Copy the code

Throughout the diff process, we need to maintain several variables: the header index I, the tail index E1 of the old child, and the tail index E2 of the new child.

To synchronize the head node is to start from the head and compare the new node and the old node in turn. If they are the same, patch will be performed to update the node. If index I is different or greater than index E1 or e2, the synchronization process ends.

2. Synchronize tail nodes

The code is as follows:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1
  
  // 1. Synchronize from the header
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d
  
  // 2. Start synchronization from the tail
  // i = 2, e1 = 3, e2 = 4
  // (a b) (c d)
  // (a b) e (c d)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    else {
      break
    }
    e1--
    e2--
  }
}
Copy the code

To synchronize the tail node is to start from the tail and compare the new node and the old node successively. If they are the same, patch will be performed to update the node. If index I is different or greater than index E1 or e2, the synchronization process ends.

3. Add new nodes

Determine whether the new child node has any remaining conditions, if so, add a new child node, the implementation code is as follows:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1

  // 1. Synchronize from the header
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d
  // ...

  // 2. Start synchronization from the tail
  // i = 2, e1 = 3, e2 = 4
  // (a b) (c d)
  // (a b) e (c d)

  // 3. Mount the remaining new nodes
  // i = 2, e1 = 1, e2 = 2
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
      while (i <= e2) {
        // Mount the new node
        patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG)
        i++
      }
    }
  }
}
Copy the code

If index I is greater than tail index E1 and I is less than e2, then between index I and e2 we directly mount the nodes in this part of the new subtree.

4. Delete redundant nodes

If the situation of adding new nodes is not satisfied, I will continue to judge whether there is any surplus of the old child node. If so, I will delete the old child node, and the implementation code is as follows:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1

  // 1. Synchronize from the header
  // i = 0, e1 = 4, e2 = 3
  // (a b) c d e
  // (a b) d e
  // ...

  // 2. Start synchronization from the tail
  // i = 2, e1 = 4, e2 = 3
  // (a b) c (d e)
  // (a b) (d e)

  // 3. The normal sequence mounts the remaining new nodes
  // i = 2, e1 = 2, e2 = 1
  / / not satisfied
  if (i > e1) {
  }
  // 4. Delete redundant old nodes from normal sequence
  // i = 2, e1 = 2, e2 = 1
  else if (i > e2) {
    while (i <= e1) {
      // Delete a node
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }
}
Copy the code

If index I is greater than tail index e2, then between index I and index E1, we simply delete the nodes in this part of the old subtree.

5. Processing unknown subsequences

Simply adding and removing nodes is ideal and easy to do, but sometimes we are not so lucky and encounter complex unknown subsequences.

5.1 Building an Index Diagram

Typically during development, each item in the v-for generated list is assigned a unique key as the unique ID of the item. This key plays a key role in the diff process. For the nodes in the old and new subsequences, we believe that the same node is the same node if the key is the same. Patch update can be performed directly.

We build the index graph of the new sub-sequence according to key, as follows:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1
  // 1. Synchronize from the header
  // i = 0, e1 = 7, e2 = 7
  // (a b) c d e f g h
  // (a b) e c d i g h
  // 2. Start synchronization from the tail
  // i = 2, e1 = 7, e2 = 7
  // (a b) c d e f (g h)
  // (a b) e c d i (g h)
  // 3. The normal sequence mounts the remaining new nodes
  // 4. Delete redundant old nodes from normal sequence
  // i = 2, e1 = 4, e2 = 5
  // The old subsequence is indexed from I
  const s1 = i
  // the new subsequence starts indexing from I
  const s2 = i

  // 5.1 Build the index graph of the new subsequence based on key
  const keyToNewIndexMap = new Map(a)for (i = s2; i <= e2; i++) {
    const nextChild = c2[i]
    keyToNewIndexMap.set(nextChild.key, i)
  }
}
Copy the code

The new and old subsequence starts from I, so we first use S1 and S2 as the starting index of the new and old subsequence respectively, and then build a Map<key, index> structure of keyToNewIndexMap to traverse the new subsequence. Add the node’s key and index to the Map

5.2 Updating and Removing old Nodes

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1
  // 1. Synchronize from the header
  // i = 0, e1 = 7, e2 = 7
  // (a b) c d e f g h
  // (a b) e c d i g h
  // 2. Start synchronization from the tail
  // i = 2, e1 = 7, e2 = 7
  // (a b) c d e f (g h)
  // (a b) e c d i (g h)
  // 3. The normal sequence mounts the remaining new nodes
  // 4. Delete redundant old nodes from normal sequence
  // i = 2, e1 = 4, e2 = 5
  // The old subsequence is indexed from I
  const s1 = i
  // the new subsequence starts indexing from I
  const s2 = i
  // 5.1 Build the index graph of the new subsequence based on key
  // 5.2 Traverses the old subsequence in positive order, finds the matching node update, deletes the node that is not in the new subsequence, and determines whether there is a moving node
  // The number of nodes updated in the new subsequence
  let patched = 0
  // The number of nodes to be updated is equal to the length of the new subsequence
  const toBePatched = e2 - s2 + 1
  // Whether there is a node to move
  let moved = false
  // Is used to trace whether a node has moved
  let maxNewIndexSoFar = 0
  // This array stores the index of the elements in the new subsequence at the node of the old subsequence, which is used to determine the longest increasing subsequence
  const newIndexToOldIndexMap = new Array(toBePatched)
  // Initialize the array with each element having a value of 0
  // 0 is a special value. If there are still elements with a value of 0 after traversal, the new node has no corresponding old node
  for (i = 0; i < toBePatched; i++)
    newIndexToOldIndexMap[i] = 0
  // traverses the old subsequence in positive order
  for (i = s1; i <= e1; i++) {
    // Get each old subsequence node
    const prevChild = c1[i]
    if (patched >= toBePatched) {
      // All new subsequence nodes have been updated, and the remaining nodes have been deleted
      unmount(prevChild, parentComponent, parentSuspense, true)
      continue
    }
    // Find the index of the node in the old subsequence in the new subsequence
    let newIndex = keyToNewIndexMap.get(prevChild.key)
    if (newIndex === undefined) {
      // If the old subsequence does not exist in the new subsequence, delete the node
      unmount(prevChild, parentComponent, parentSuspense, true)}else {
      // Update the index of the element in the new subsequence in the old subsequence. The offset is added by 1 to avoid the special case where I is 0, which affects the solution of the longest increasing subsequence
      newIndexToOldIndexMap[newIndex - s2] = i + 1
      // maxNewIndexSoFar will always store the newIndex of the last evaluation
      if (newIndex >= maxNewIndexSoFar) {
        maxNewIndexSoFar = newIndex
      }
      else {
        moved = true
      }
      // Update matched nodes in the old and new subsequences
      patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
      patched++
    }
  }
}
Copy the code

We created an array of newIndexToOldIndexMap to store the mapping between the index of the new subsequence node and the index of the old subsequence node, used to determine the longest increasing subsequence. The length of this array is the length of the new subsequence. The initial value of each element is set to 0, which is a special value. If there is still an element with a value of 0, then the node was not processed during the traversal of the old subsequence and is newly added.

The specific operation process is as follows: the old subsequence is traversed forward, and the index of the node in the old subsequence in the new subsequence is searched according to the previously established keyToNewIndexMap. If the node cannot be found, it means that the node does not exist in the new subsequence and is deleted. If it finds it, update its index in the old subsequence to newIndexToOldIndexMap.

Note that an offset of length 1 is added to the index to deal with the special case where I is 0, otherwise it will affect the subsequent solution of the longest increasing subsequence.

In the traversal process, we use the variable maxNewIndexSoFar to track and judge whether the node moves. MaxNewIndexSoFar always stores the newIndex evaluated last time. Once the newIndex evaluated this time is less than maxNewIndexSoFar, This indicates that the index of the node traversing the old subsequence is not always increasing in the new subsequence, which indicates that there is movement.

In addition, in this process, we will also update matched nodes in the old and new subsequences. In addition, if all new subsequence nodes have been updated, but the traversal of the old subsequence is not finished, it means that the remaining nodes are redundant and can be deleted.

So far, we have completed the update of the old and new subsequence nodes, deleted the redundant old nodes, and established a newIndexToOldIndexMap to store the mapping between the indexes of the new subsequence nodes and the indexes of the old subsequence nodes, and determine whether there is movement.

5.3 Moving and Mounting a Node

Moving and mounting the new node is the final process for dealing with unknown subsequences. Let’s look at the implementation of this part of logic:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) = > {
  let i = 0
  const l2 = c2.length
  // The tail index of the old child node
  let e1 = c1.length - 1
  // The tail index of the new child node
  let e2 = l2 - 1
  // 1. Synchronize from the header
  // i = 0, e1 = 6, e2 = 7
  // (a b) c d e f g
  // (a b) e c d h f g
  // 2. Start synchronization from the tail
  // i = 2, e1 = 6, e2 = 7
  // (a b) c (d e)
  // (a b) (d e)
  // 3. The normal sequence mounts the remaining new nodes
  // 4. Delete unnecessary nodes from common sequence
  // i = 2, e1 = 4, e2 = 5
  // The old child node starts indexing from I
  const s1 = i
  // The new child node starts indexing from I
  const s2 = i //
  // 5.1 Build the index graph of the new subsequence based on key
  // 5.2 Traverses the old subsequence in positive order, finds the matching node update, deletes the node that is not in the new subsequence, and determines whether there is a moving node
  // 5.3 Moving and mounting a new node
  // Generate the longest increasing subsequence only when the node moves
  const increasingNewIndexSequence = moved
    ? getSequence(newIndexToOldIndexMap)
    : EMPTY_ARR
  let j = increasingNewIndexSequence.length - 1
  // Iterate backwards so that we can use the last updated node as the anchor
  for (i = toBePatched - 1; i >= 0; i--) {
    const nextIndex = s2 + i
    const nextChild = c2[nextIndex]
    // The anchor points to the last updated node, or parentAnchor if nextIndex exceeds the length of the new child node
    const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor
    if (newIndexToOldIndexMap[i] === 0) {
      // Mount the new child node
      patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG)
    }
    else if (moved) {
     // There is no longest increment subsequence (reverse scenario) or the current node index is not in the longest increment subsequence and needs to be moved
      if (j < 0|| i ! == increasingNewIndexSequence[j]) { move(nextChild, container, anchor,2)}else {
        // reverse increment subsequence
        j--
      }
    }
  }
}
Copy the code

If moved is true, calculate the longest increasing subsequence by getSequence(newIndexToOldIndexMap). The new subsequence is traversed in reverse order, because reverse order traversal can facilitate us to use the last updated node as the anchor point. In the reverse order process, the anchor point points to the last updated node, and then determine whether newIndexToOldIndexMap[I] is 0. If so, it indicates that this is a new node and needs to mount it. Then the index of the node is in the longest increasing subsequence. If so, the index of the node is in the reverse order of the longest increasing subsequence. Otherwise, it is moved to the front of the anchor point.

Refer to the article

  • Vue. Js 3.0 core source code internal reference