In the last article, we introduced the rendering process of the component. This article introduces the updating rendering process of the component after the responsive data changes. Finally, there is an analysis summary diagram without looking at the article.

case

To illustrate the process, here is an example:

  • AppComponentHelloComponent, and assign valuesmsgthispropValues forHelloComponents;
  • whenmsgforVue 3When,AppIn the componentliLabel array displayvue3.featureThat showsVue 3New features whenmsgforVue 2Is not displayed;
  • AppThere is a button toggle in the componentmsgThe value of the.
App.vue
<template> <HelloWorld: MSG =" MSG "/> <h1>App component display :</h1> <ul> <li v-for="item in vue3. Feature" v-bind:key="item">{{item < span style =" box-sizing: border-box; color: RGB (255, 255, 255); line-height: 22px; white-space: normal;" ref } from "vue"; import HelloWorld from "./components/HelloWorld.vue"; export default defineComponent({ name: "App", components: { HelloWorld, }, setup() { const msg = ref("Vue 2"); const feature3: string[] = ["reactive", "composition api", "setup", "toRef", "Teleport"]; const feature2: string[] = ["reactive", "option api"]; const vue3 = reactive({ feature: feature2}); let current = 0; const changeMsg = () => { if (current == 0) { msg.value = "Vue 3"; vue3.feature = feature3; current = 1; } else { msg.value = "Vue 2"; vue3.feature = feature2; current = 0; }}; return { msg, vue3, changeMsg, }; }}); </script>Copy the code
Hello.vue
<template> <h1>Hello component display :{{MSG}}</h1> </template> <script lang="ts"> import {ref, defineComponent} from "vue"; export default defineComponent({ name: "HelloWorld", props: { msg: { type: String, required: true, }, }, setup: () => {},}); </script>Copy the code
The renderings are shown below

Side effect Render functioncomponentUpdateFnEnable component rerender

We mentioned in the previous article that when a component is mounted it creates a side effect rendering function componentUpdateFn, which is called after a responsive data change.

Why would a change in data cause the side effect of a render function to be called? This is the relevant content of Vue 3.0 responsive system, which will be introduced later. That’s the logic for now.

const componentUpdateFn = () => { // 1. if (! instance.isMounted) { instance.isMounted = true } else { let { next, bu, u, parent, vnode } = instance let originNext = next let vnodeHook: VNodeHook | null | undefined // 2. if (next) { next.el = vnode.el updateComponentPreRender(instance, next, optimized) } else { next = vnode } // 3 const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree  instance.subTree = nextTree // 4 patch( prevTree, nextTree, // parent may have changed if it's in a teleport hostParentNode(prevTree.el!) ! , // anchor may have changed if it's in a fragment getNextHostNode(prevTree), instance, parentSuspense, isSVG ) } }Copy the code
  1. componentUpdateFnThe mount logic is executed only the first timeisMountedBe set totrue, followed by the logic to perform the update;
  2. When components update themselves,nextIs empty, will benextPointing to the component object itselfvnode;
  3. renderComponentRootUpdate the subtreeVNodeIn this example, the main subtreeVNodeThe first and third ofVNodeTo update the data;

4. patchUsed to compare old and new subtreesVNodeTo find the right way to updateDOM.

patchUpdate the logic of the component

const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating ? false : !! n2.dynamicChildren ) => { // 1. if (n1 === n2) { return } // 2. if (n1 && ! isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, // 3. Const {type, ref, shapeFlag} = n2 switch (type) {// omit... default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, Anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)} }}Copy the code
  1. If the old and newVNodeIf the node is the same, it will be returned without any processing.
  2. If the old and newVNodeIf the type of node is different, it will be oldVNodeThe node is unloaded and then the old one is removedVNodeIf the node is empty, mount the logic.
  3. If the old and newVNodeThe types of nodes are the sameVNodeTypes walk different update logic, such as components walkprocessComponentProcess, normalDOMElement node walkprocessElementProcess.

The first child node in this example is the componentVNodeNode goprocessComponentThe othersVNodeNode goprocessElementProcess.

Child component update processupdateComponent

The first child VNode of the VNode subtree of the App component object, VNode, is the VNode of the Hello component object. Its Prop value has changed, so the Hello component object needs to update its rendering. Next, let’s look at processComponent, the update logic of the Hello component.

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  // 1.
  if (shouldUpdateComponent(n1, n2, optimized)) {
    // 2.   
    instance.next = n2
    // 3.
    invalidateJob(instance.update)
    // 4.
    instance.update()
  } else {
    // 2.
    n2.component = n1.component
    n2.el = n1.el
    instance.vnode = n2
  }
}

Copy the code
  1. The first to useshouldUpdateComponentDetermine if the component needs to be rerendered because someVNodeA change in the value does not need to show an update immediately. The updated conditions includepropandchildrenChange, etc.;
  2. Set for the component objectnextValue, that is, if the component updates itself, it is not setnextIf the parent triggers the update, the child component object has this setnextValue;

3. Cancel the update of child component objects in the update queue to avoid repeated updates; 4. Subcomponent side effect rendering functionscomponentUpdateFnIs called, into another round of recursive calls;

  • Question: Why do subcomponent objects need to be set to rerendernextValue?
  • Answer: At this point the child component object does not know it needs to be updated toVNodeAll that needs to be assigned to the child component object to let it know how to update the rendering.

The side effect rendering function of the child triggered by the parent componentcomponentUpdateFnAnd the component itself

let { next, bu, u, parent, vnode } = instance
let originNext = next
        
if (next) {
  next.el = vnode.el
  updateComponentPreRender(instance, next, optimized)
} else {
  next = vnode
}
Copy the code

The difference is that the VNode of the child component triggered by the parent object has the next value. In this case, we need to execute updateComponentPreRender to assign props, slot, and other properties before rendering.

  • Question: Why shouldn’t the rendering triggered by the component object itself be performedupdateComponentPreRenderMethods?
  • Answer: The component object was already executed when it was mountedupdateComponentPreRenderMethod, so you only need to update some property values in the case of self-triggered, or passupdateComponentPreRender, or directly to the SettingsvnodeAttribute values.

Normal element node update entrypatchElement

const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  // 1.
  patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
  // 2. 
  patchChildren(
    n1,
    n2,
    el,
    null,
    parentComponent,
    parentSuspense,
    areChildrenSVG,
    slotScopeIds,
    false
  )
}

Copy the code

Update props, styles, classes, events, etc.; Update a child node using patchChildren.

Next, we will focus on the update logic of the sub-nodes.

The child node of a normal element node is updatedpatchChildren

const patchChildren: PatchChildrenFn = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized = false ) => { // 1. const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const c2 = n2.children const { patchFlag, shapeFlag } = n2 if (patchFlag > 0) { // 2. if (patchFlag & PatchFlags.KEYED_FRAGMENT) { patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // unkeyed patchUnkeyedChildren( c1 as VNode[],  c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } } if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 3. if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { unmountChildren(c1 as VNode[], parentComponent, parentSuspense) } if (c2 ! == c1) { hostSetElementText(container, c2 as string) } } else { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 4. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true) } } else { // 5. if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) { hostSetElementText(container, '') } if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } } } }Copy the code

There are three cases of children of a normal element node:

Child node type example
Array child node <ul><li>1</li><li>1</li><li>1</li></ul>
Text child node Text < div > < / div >
Loophole node <img />
patchChildrenFor these three cases, there are nine cases:
Row – old node, column – new node Array child node
Array child node The diff than
Text child node Replace text nodes with array nodes
Loophole node Mount the array child nodes

There is nov-keyThe alignment of the children of an arraypatchUnkeyedChildren

const patchUnkeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  c1 = c1 || EMPTY_ARR
  c2 = c2 || EMPTY_ARR
  const oldLength = c1.length
  const newLength = c2.length
  const commonLength = Math.min(oldLength, newLength)
  let i
  for (i = 0; i < commonLength; i++) {
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    patch(
      c1[i],
      nextChild,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
  if (oldLength > newLength) {
    // remove old
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength
    )
  } else {
    // mount new
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized,
      commonLength
    )
  }
}
Copy the code

This method has simple logic: first, patch the two arrays one by one from front to back. When the comparison of an array is completed, mount the remaining nodes by mountChildren if there are still elements in the new child node array, and unmountChildren if there are remaining elements in the old node.

This method is simple, but less efficient. We then analyze efficient comparison methods.

There arev-keyEfficient alignment of array child nodespatchKeyedChildren

This logic is very long, so let’s break it down:

1. Synchronize the header node

Old node (a b) C

New node (a b) d E

Start from the header of the two arrays. If the node is of the same VNode type, perform patch to update the node. Otherwise, synchronization ends. The third node in the above example is the logical end of synchronizing the header node.

let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // prev ending index
let e2 = l2 - 1 // next ending index

// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
  const n1 = c1[i]
  const n2 = (c2[i] = optimized
    ? cloneIfMounted(c2[i] as VNode)
    : normalizeVNode(c2[i]))
  if (isSameVNodeType(n1, n2)) {
    patch(
      n1,
      n2,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    break
  }
  i++
}

Copy the code
2. Synchronize nodes

// a (b c)

// d e (b c)

Start with the ends of the two arrays. If the nodes are of the same VNode type, perform patch to update the nodes. Otherwise, synchronize the ends. In the example above, the logic ends when the third to last node synchronizes the tail node.

// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
  const n1 = c1[e1]
  const n2 = (c2[e2] = optimized
    ? cloneIfMounted(c2[e2] as VNode)
    : normalizeVNode(c2[e2]))
  if (isSameVNodeType(n1, n2)) {
    patch(
      n1,
      n2,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    break
  }
  e1--
  e2--
}
Copy the code
3. The new child node array has new child nodes that need to be added

(a b)

(a b) c

If (I > e1) {if (I <= e2) {// nextPos = e2 + 1 const Anchor = nextPos < l2? While (I <= e2) {patch(null, (c2[I] = optimized? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) i++ } } }Copy the code
4. The old child node array has child nodes that need to be unloaded

(a b) c (d e)

(a b) (d e)

else if (i > e2) {
  while (i <= e1) {
    unmount(c1[i], parentComponent, parentSuspense, true)
    i++
  }
}
Copy the code
5. Process unknown subsequences

// [i … e1 + 1]: a b [c d j] f g

// [i … e2 + 1]: a b [e d c h] f g

// i = 2, e1 = 4, e2 = 5

  • 1. Create an index graph of the new subsequence — the index value of each node in the unknown new subsequence
// 5.1 build key:index map for newChildren const keyToNewIndexMap: Map<string | number, number> = new Map() for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key ! = null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } }Copy the code

Results:

{
    {"e" => 2},
    {"d" => 3},
    {"c" => 4},
    {"h" => 5}
}
Copy the code
  • 2. Iterate over the old subsequence and execute if there is the same keypatchUpdate, and remove nodes that are not in the new subsequence, and determine if the sequence has changed in order.
  • To build anewIndexToOldIndexMapArray, the length of the array isUnknown new subsequenceThe initial value of each element is 0. When the final processing is 0, it means that this node is a new node.
  • The index of the node in the old subsequence is found in the new subsequence. If the node is not found in the new subsequence, the node needs to be unloaded. If found, its index from the old subsequence is updated to newIndexToOldIndexMap ‘, with the index added by 1;
  • usingmaxNewIndexSoFarTo calculate whether the order of new child nodes has been replaced, if so, will be replacedmovedSet totrue;
  • If the new child node sequence has been traversed and the old child node still has elements, you can simply unload the node.
let j let patched = 0 const toBePatched = e2 - s2 + 1 let moved = false // used to track whether any node has moved let maxNewIndexSoFar = 0 // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no  corresponding old node. // used for determining longest stable subsequence const newIndexToOldIndexMap = new Array(toBePatched) for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 for (i = s1; i <= e1; i++) { const prevChild = c1[i] if (patched >= toBePatched) { // all new children have been patched so this can only be a  removal unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex if (prevChild.key ! = null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break } } } if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { newIndexToOldIndexMap[newIndex - s2] = i + 1 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { moved = true } patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) patched++ } }Copy the code

Results:

NewIndexToOldIndexMap: [0, 4, 3, 0] // d in the old child is 3, c in the old child is 2, e,h are new nodes moved: trueCopy the code
  • 3. Move and mount child nodes
  • ifmovedfortrue, the maximum increasing subsequence is solvedincreasingNewIndexSequence, the maximum increment subsequence can minimize the number of moves;

In this example, the values obtained are [0, 2], which represents 0, 3 for newIndexToOldIndexMap.

  • If the value of the index corresponding to newIndexToOldIndexMap is 0, it indicates that the new node is added. Then mount the node.
  • The new child node is traversed in reverse orderincreasingNewIndexSequenceThe value of the element under the corresponding index should be moved, otherwise no operation is performed.

Let’s use the example above to explain:

cycles New child node index The new child node The index of the increasingNewIndexSequence IncreasingNewIndexSequence [index] NewIndexToOldIndexMap [Loop number] Operation performed
1 5 h 1 3 0 Mount h directly
2 4 c 1 3 3 C will not operate, increasingNewIndexSequence index 1, 0
3 3 d 0 0 4 You take d, you move it in front of c
4 2 e 0 0 0 Mount E directly

conclusion