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:
App
ComponentHello
Component, and assign valuesmsg
thispropValues forHello
Components;- when
msg
forVue 3When,App
In the componentli
Label array displayvue3.feature
That showsVue 3
New features whenmsg
forVue 2Is not displayed; App
There is a button toggle in the componentmsg
The 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 functioncomponentUpdateFn
Enable 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
componentUpdateFn
The mount logic is executed only the first timeisMounted
Be set totrue
, followed by the logic to perform the update;- When components update themselves,
next
Is empty, will benext
Pointing to the component object itselfvnode
;renderComponentRoot
Update the subtreeVNode
In this example, the main subtreeVNode
The first and third ofVNode
To update the data;4.
patch
Used to compare old and new subtreesVNode
To find the right way to updateDOM.
patch
Update 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
- If the old and new
VNode
If the node is the same, it will be returned without any processing.- If the old and new
VNode
If the type of node is different, it will be oldVNode
The node is unloaded and then the old one is removedVNode
If the node is empty, mount the logic.- If the old and new
VNode
The types of nodes are the sameVNode
Types walk different update logic, such as components walkprocessComponent
Process, normalDOMElement node walkprocessElement
Process.The first child node in this example is the component
VNode
Node goprocessComponent
The othersVNode
Node goprocessElement
Process.
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
- The first to use
shouldUpdateComponent
Determine if the component needs to be rerendered because someVNode
A change in the value does not need to show an update immediately. The updated conditions includeprop
andchildren
Change, etc.;- Set for the component object
next
Value, that is, if the component updates itself, it is not setnext
If the parent triggers the update, the child component object has this setnext
Value;3. Cancel the update of child component objects in the update queue to avoid repeated updates; 4. Subcomponent side effect rendering functions
componentUpdateFn
Is called, into another round of recursive calls;
- Question: Why do subcomponent objects need to be set to rerender
next
Value? - Answer: At this point the child component object does not know it needs to be updated to
VNode
All 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 componentcomponentUpdateFn
And 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 performed
updateComponentPreRender
Methods? - Answer: The component object was already executed when it was mounted
updateComponentPreRender
Method, so you only need to update some property values in the case of self-triggered, or passupdateComponentPreRender
, or directly to the Settingsvnode
Attribute 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 />
patchChildren
For 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 key
patch
Update, and remove nodes that are not in the new subsequence, and determine if the sequence has changed in order.
- To build a
newIndexToOldIndexMap
Array, 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;
- using
maxNewIndexSoFar
To calculate whether the order of new child nodes has been replaced, if so, will be replacedmoved
Set 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
- if
moved
fortrue, 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 order
increasingNewIndexSequence
The 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 |