preface

Hello everyone, I am Delai ask, last time, we talked about the first render part, students who have questions about the last article, welcome to leave a message, we discuss together, study together; This article will focus on what happens when data is updated in Vue3.

The iPhone12 was released, and after looking at the price, I think I’ll get another year out of my iPhone7 plus.

Webpack5 was released, two years later, but the new version of the release, there are usually some bugs, and so on all the features are perfect, we will upgrade to use it.

The example code

This article will cover dom diff, so let’s use the following example, which builds on the previous article by adding a data change to the list. HTML adds a change button that changes the list value by calling the change function by clicking the change button. Example is located in the source code/packages/vue/examples/classic/directory, here are examples of code:

const app = Vue.createApp({
    data() {
        return {
            list: ['a'.'b'.'c'.'d']}},methods: {
        change() {
            this.list = ['a'.'d'.'e'.'b']}}}); app.mount('#demo')
Copy the code
<! DOCTYPEhtml>
<html>
<head>
    <meta name="viewport"
          content="Initial - scale = 1.0, the maximum - scale = 1.0, the minimum - scale = 1.0, user - scalable = no, target - densitydpi = medium - dpi, viewport - fit = cover"
    />
    <title>Vue3.js hello example</title>

    <script src=".. /.. /dist/vue.global.js"></script>
</head>
<body>
<div id="demo">
    <ul>
        <li v-for="item in list" :key="item">
            {{item}}
        </li>
    </ul>
    <button @click="change">change</button>
</div>
<script src="./hello.js"></script>
</body>
</html>
Copy the code

The source code interpretation

As for the change of data in Vue3 and the process of page change, this article only explains componentEffect and subsequent code, how to implement componentEffect function after data change, and why to implement componentEffect. This will be explained in a later article.

componentEffect

Take a look at the code for componentEffect update:

  // @file packages/runtime-core/src/renderer.ts
  function componentEffect() {
    if(! instance.isMounted) {// first render
    } else {
        let {next, bu, u, parent, vnode} = instance
        let originNext = next
        let vnodeHook: VNodeHook | null | undefined

        if (next) {
            updateComponentPreRender(instance, next, optimized)
        } else {
            next = vnode
        }
        next.el = vnode.el

        // beforeUpdate hook
        if (bu) {
            invokeArrayFns(bu)
        }
        // onVnodeBeforeUpdate
        if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
            invokeVNodeHook(vnodeHook, parent, next, vnode)
        }
        const nextTree = renderComponentRoot(instance)
        const prevTree = instance.subTree
        instance.subTree = nextTree

        if(instance.refs ! == EMPTY_OBJ) { instance.refs = {} } patch( prevTree, nextTree, hostParentNode(prevTree.el!) ! , getNextHostNode(prevTree), instance, parentSuspense, isSVG ) next.el = nextTree.elif (originNext === null) {
            updateHOCHostEl(instance, nextTree.el)
        }
        // updated hook
        if (u) {
            queuePostRenderEffect(u, parentSuspense)
        }
        // onVnodeUpdated
        if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
            queuePostRenderEffect(() = >{ invokeVNodeHook(vnodeHook! , parent, next! , vnode) }, parentSuspense) } } }Copy the code

When the data changes, it ends up in the else logic above.

  • By default, next is null. The parent’s call to processComponent triggers the current call as VNode, and next is null.
  • Call the current instance beforeUpdate hook function; Calls the beforeUpdate hook function of the parent component of the Vnode(Next) to be updated;
  • Get the vNode of the current instance => prevTree; Get the vNode to update => nextTree; Then call patch;

The process of calling patch function, that is, the process of going through different tributaries according to the type of VNode; Click the change button: n1 value:N2 value:Based on this value, you can tell that the processFragment function is going to be called;

processFragment

Call processFragment(n1, n2, Container, Anchor, parentComponent, parentSuspense, isSVG, Optimized)

  • N1 and n2 are shown in the figure above;
  • The container # for the demo;
  • The anchor is null;
  • ParentComponent is instance;
  • ParentSuspense is null;
  • IsSVG is false;
  • Optimized to false;

Take a look at the processFragment function source code:

// @file packages/runtime-core/src/renderer.ts
const processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) = > {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(' '))!
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(' '))!

    let {patchFlag, dynamicChildren} = n2
    if (patchFlag > 0) {
        optimized = true
    }

    if (n1 == null) {
    	// first render
    } else {
        if (
            patchFlag > 0&& patchFlag & PatchFlags.STABLE_FRAGMENT && dynamicChildren ) { patchBlockChildren( n1.dynamicChildren! , dynamicChildren, container, parentComponent, parentSuspense, isSVG )if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
                traverseStaticChildren(n1, n2)
            } else if( n2.key ! =null ||
                (parentComponent && n2 === parentComponent.subTree)
            ) {
                traverseStaticChildren(n1, n2, true /* shallow */)}}else {
            patchChildren(
                n1,
                n2,
                container,
                fragmentEndAnchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
            )
        }
    }
}
Copy the code

After stripping out the first Render code, you can see that there are still two branches below; According to n1 and N2, we will go to the if branch and execute patchBlockChildren.

patchBlockChildren

PatchBlockChildren (n1.dynamicChildren, n2.dynamicChildren, container, parentComponent, parentSuspense, isSVG) The parameters are as follows:

  • OldChildren: n1. DynamicChildren, i.e. Symbol(Fragment) =>ul and button;
  • NewChildren: n2.dynamicChildren => Symbol(Fragment) ul and button;
  • FallbackContainer: container (#demo);
  • ParentComponent: instance instance;
  • ParentSuspense: null;
  • IsSVG: false.

PatchBlockChildren patchBlockChildren patchBlockChildren

// @file packages/runtime-core/src/renderer.ts
const patchBlockChildren: PatchBlockChildrenFn = (oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG) = > {
    for (let i = 0; i < newChildren.length; i++) {
        const oldVNode = oldChildren[i]
        const newVNode = newChildren[i]
        constcontainer = oldVNode.type === Fragment || ! isSameVNodeType(oldVNode, newVNode) || oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!) ! : fallbackContainer patch( oldVNode, newVNode, container,null,
            parentComponent,
            parentSuspense,
            isSVG,
            true)}}Copy the code

It can be seen that patchBlockChildren is a for loop calling the patch function. It can be seen that newChildren is an array of length 2. Loop through patch(oldVNode, newVNode, Container, NULL, parentComponent, parentSuspense, isSVG, true);

  • First loop:
    • OldVNode: VNode object generated by the old UL array;
    • NewVNode: VNode object generated by the new UL array;
    • Container: ul element.
    • Anchor: Null is passed above;
    • ParentComponent: instance instance;
    • ParentSuspense: null;
    • IsSVG: false;
    • Optimized: true,
  • Second loop:
    • OldVNode: VNode object made up of the old change button;
    • NewVNode: VNode object made up of the new change button;
    • Container: The container is #demo.
    • Anchor: Null is passed above;
    • ParentComponent: instance instance;
    • ParentSuspense: null;
    • IsSVG: false;
    • Optimized: true,

processElement

Let’s do the second cycle first, the second cycle is easier; In the second loop, the type of newVNode is button. The processElement is passed through:

const processElement = (
        n1: VNode | null,
        n2: VNode,
        container: RendererElement,
        anchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
    ) = > {
        isSVG = isSVG || (n2.type as string) = = ='svg'
        if (n1 == null) {
            // first render
        } else {
            patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
        }
    }
Copy the code

As shown in the above code, patchElement will be directly called with the parameter:

  • N1: VNode object composed of the old change button;
  • N2: VNode object formed by the new change button;
  • ParentComponent: instance instance;
  • ParentSuspense: null;
  • IsSVG: false;
  • Optimized: true,

patchChildren

Patch: newVNode: Symbol(Fragment) => ul The patchChildren function will continue to run.

patchChildren

At this point, the patchChildren function is run. Let’s look at the parameters at this point:

  • N1: VNode object generated by the old UL array;
  • N2: VNode object generated by the new UL array;
  • Container: ul element.
  • Anchor: object generated at ul end;
  • ParentComponent: instance instance;
  • ParentSuspense: null
  • IsSVG: false;
  • Optimized: true,

PatchChildren patchChildren patchChildren patchChildren

const patchChildren: PatchChildrenFn = ( 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 {patchFlag, shapeFlag} = n2 if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // patchUnkeyedChildren return } } // other . }Copy the code

At this time, the value of patchFlag is 128, and our list rendering has key, so will run patchKeyedChildren function, c1 is an array of four Li’s (a, B, C, D); C2 is the array of the new Li (a, D,e,b); Other values were transparently transmitted to patchKeyedChildren.

patchKeyedChildren

The parameters of the patchKeyedChildren function have been described above. Here we review again:

  • C1: array of four Li’s (a,b,c,d);
  • C2: array of the new Li (a, D,e,b);
  • Container: ul element.
  • ParentAnchor: object generated at ul end;
  • ParentComponent: instance instance;
  • ParentSuspense: null
  • IsSVG: false;
  • Optimized: true,

PatchKeyedChildren: patchKeyedChildren: patchKeyedChildren

const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
) = > {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 
    let e2 = l2 - 1 

    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,optimized)
        } else {
            break
        }
        i++
    }

    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,optimized)
        } else {
            break
        }
        e1--
        e2--
    }

    if (i > e1) {
        if (i <= e2) {
            const nextPos = e2 + 1
            const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
            while (i <= e2) {
                patch(
                    null,
                    (c2[i] = optimized
                        ? cloneIfMounted(c2[i] as VNode)
                        : normalizeVNode(c2[i])),
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG
                )
                i++
            }
        }
    }

    else if (i > e2) {
        while (i <= e1) {
            unmount(c1[i], parentComponent, parentSuspense, true)
            i++
        }
    }

    else {
        const s1 = i
        const s2 = i 
        for (i = s2; i <= e2; i++) {
            const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i]))
            if(nextChild.key ! =null) {
                keyToNewIndexMap.set(nextChild.key, i)
            }
        }

        let j
        let patched = 0
        const toBePatched = e2 - s2 + 1
        let moved = false
        let maxNewIndexSoFar = 0
        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) {
                unmount(prevChild, parentComponent, parentSuspense, true)
                continue
            }
            let newIndex
            if(prevChild.key ! =null) {
                newIndex = keyToNewIndexMap.get(prevChild.key)
            } else {
                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,
                    optimized
                )
                patched++
            }
        }

        const increasingNewIndexSequence = moved
            ? getSequence(newIndexToOldIndexMap)
            : EMPTY_ARR
        j = increasingNewIndexSequence.length - 1
        for (i = toBePatched - 1; i >= 0; i--) {
            const nextIndex = s2 + i
            const nextChild = c2[nextIndex] as VNode
            const anchor =
                nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
            if (newIndexToOldIndexMap[i] === 0) {
                patch(
                    null,
                    nextChild,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG
                )
            } else if (moved) {
                if (j < 0|| i ! == increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) }else {
                    j--
                }
            }
        }
    }
}
Copy the code

The code above contains two while loops and two if-else pairs;

  • I =0, the cycle starts subscript; E1 and e2 are the lengths of C1 and C2. L2 is the length of the new children;
  • The first while loop iterates through the list from scratch:
    • When nodeType is the same, patch is called;
    • When the nodeType is different, the loop is broken;
  • The second while loop iterates from the end when the first while loop has not iterated through either c1 or c2:
    • When nodeType is the same, patch is called;
    • When the nodeType is different, the loop is broken;
  • The first if, I >e1 proves that C1 has been traversed, and I <=e2 proves that C2 has not been traversed. Patch is called to continue traversing the remaining C2.
  • Second else – if I > e2 prove that c2 have finished traversal, I < = e1 prove c1 haven’t traverse through, for the rest of the c1 continue to traverse, for c1 old list, call the unmount to list uninstall the useless:
  • Second else: c1 and c2 have at least one else that has not been iterated to the last else:
    • for (i = s2; i <= e2; i++)The for loop iterates through the remaining C2 and collects the key of each C2 element to form a map => keyToNewIndexMap;
    • for (i = 0; i < toBePatched; i++)The for loop iterates over the remainder of the length of C2 to generate the map and assigns a value of 0;
    • for (i = s1; i <= e1; i++) The for loop iterates over the remaining C1 and directly retrieves it using key (the for loop retrieves the remaining C2)newIndex,So again, we’re going to bind the key, so uniqueness is important; The value of newIndex means that c2 has the current old element in C1, and the old preChild is still needed in C2patch; If newIndex is undefined, the old preChild is no longer needed in C2. Call unmount to unmount the current preChild.
    • After traversing the remaining C1, rewind the remaining C2:for (i = toBePatched - 1; i >= 0; i--); if(newIndexToOldIndexMap[i] === 0Then prove that the current nextChild is the new node and call patch. Otherwise, determine whether or not a move moved occurred before, and then, logically, call move;

PatchKeyedChildren example

According to the above example, we by the old: [‘ a ‘, ‘b’, ‘c’, ‘d’] change to new: [‘ a ‘, ‘d’, ‘e’, ‘b’] process is as follows:

  • The first while loop is entered, where I is 0, L2 is 4, E1 is 3, and e2 is 3.
    • In the first loop, old-a and new-a are the same, and patch is called without any change.
    • For the second loop, old-b is different from new-b, break;
    • Jump out of the loop and end the loop from the beginning;
  • Enter the second while loop, where I is 1, L2 is 4, E1 is 3, e2 is 3;
    • For the first loop, old-d and new-b are different, break;
    • Out of the loop, the loop that started at the tail ends;
  • I’m going to go to the first if and say false, I’m going to go to the second else if and say false, I’m going to go to else;
  • For loop collects the key of each c2 element, keyToNewIndexMap = [‘d’ => 1, ‘e’ => 2, ‘b’ => 3];
  • NewIndexToOldIndexMap = [0, 0, 0];
  • At this time to enterfor (i = s1; i <= e1; i++) The for loop iterates through the remaining c1 phase, where I is 1, S1 is 1, and S2 is 1:
    • The first loop: the traversal element is old-b, which is found to exist in new. The index in new is obtained by keyToNewIndexMap. Call the patch;
    • (c1 = [‘a’, ‘b’, ‘d’]) (c1 = [‘a’, ‘b’, ‘d’])
    • The third loop: the traversal element is old-d, which exists in new, and the index in new is 1 obtained by keyToNewIndexMap. Call the patch;
    • Jump out of the loop and iterate over the remaining phase of C1;
  • At this time to enterfor (i = toBePatched - 1; i >= 0; i--)At this point, I is 2, J is 0, S1 is 1, S2 is 1, and newIndexToOldIndexMap is [4, 0, 2] :
    • In the first loop, determine whether the current nextChild(new-b) exists or not. Through newIndexToOldIndexMap, find that nextChild exists, and the index value in old is 2, j–, where j is -1. I –, I is 1;
    • In the second loop, determine whether the current nextChild(new-e) exists or not. Through newIndexToOldIndexMap, find that the index value of nextChild is 0, indicating that it does not exist, then call patch. I — I is 0; C1 = [‘a’, ‘e’, ‘b’, ‘d’];
    • For the third loop, determine whether the current nextChild(new-d) exists, and find the index value of nextChild is 4 through newIndexToOldIndexMap, indicating the existence, then call move. I — I is -1; C1 = [‘a’, ‘d’ ‘e’, ‘b’];
    • At this point I is negative 1, it breaks out of the loop and the loop ends
  • New: [‘a’, ‘d’, ‘e’, ‘b’]

isSameVNodeType

In order to improve the performance of the page, the dom diff speed should be kept the same if there are no elements that have changed. Do not write v-for=”(item, index) in list” :key=”index”, because the index value changes when only elements inside the array are moved but the elements are not changed, which can lead to a misunderstanding in dom diff. The uniqueness of the key is important

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
    return n1.type === n2.type && n1.key === n2.key
}
Copy the code

conclusion

This chapter mainly explains the things Vue3 does in the update operation, focusing on the operation in THE DOM diff, but also gives an example for everyone to understand, you can compare with the DIff of Vue2, Vue3 has optimized the DOM DIff and has higher performance than Vue2.

The end of this chapter is also a general overview of Vue3’s main process. Later articles will explain ComponentAPI and Reactive, etc.

In fact, the epidemic situation in Qingdao is not serious, just to give everyone a peace of mind, do a good job of protection, do what you should do or do what you should do, a national nucleic acid test, make non-Qingdao people panic, do not be afraid, trust Qingdao, trust the government, trust China.