前两节讲了组件的创建和首次将DOM VNode挂载到页面的过程,其中当我们页面数据发生变化时候,会调用副作用渲染函数更新组件,这次来看下组件更新部分的逻辑,主要做了三件事情:

  • 更新组件VNode节点

  • 重新生成subTree子树VNode

  • 根据新旧VNode执行patch逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    // 运行副作用渲染函数
    // packages/runtime-core/src/renderer.ts
    const setupRenderEffect: SetupRenderEffectFn = (
    // 组件实例
    instance,
    // 需要挂载的vnode
    initialVNode,
    // 挂载容器
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
    ) => {
    instance.update = effect(function componentEffect() {
    if (!instance.isMounted) {
    // 组件挂载
    } else {
    // 组件更新逻辑
    let { next, bu, u, parent, vnode } = instance
    // 新的组件vnode,涉及组件的更新策略
    // 组件数据变化可能有两种,一种是组件本身的数据变化引起,next就不存在,将之前的vnode赋值给next。另外一种是父组件更新过程中遇到子组件节点
    // 先判断子组件是否需要更新,如果需要先执行子组件的重新渲染方法,这种情况下next就是子组件的vnode
    if (next) {
    next.el = vnode.el
    // 更新组件vnode节点信息
    updateComponentPreRender(instance, next, optimized)
    } else {
    // 没有新的组件vnode,则将之前的vnode指向next
    next = vnode
    }
    // 组件更新,重新根据组件实例生成subtree,组件里面的子组件也会生成新的subtree
    const nextTree = renderComponentRoot(instance)
    // 缓存旧的子树
    const prevTree = instance.subTree
    // 更新子树vnode
    instance.subTree = nextTree
    // 组件更新核心逻辑,根据新旧vnode做patch
    patch(
    prevTree,
    nextTree,
    // parent may have changed if it's in a teleport
    // 如果在teleport中父节点可能已经改变,所以容器直接找旧的子树dom元素的父节点
    hostParentNode(prevTree.el!)!,
    // anchor may have changed if it's in a fragment
    // 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
    getNextHostNode(prevTree),
    instance,
    parentSuspense,
    isSVG
    )
    // 缓存更新后的DOM节点
    next.el = nextTree.el
    }
    }
    }

    假设有以下的Vue代码,我们来分析一下更新过程。
    很简单,当我们点击“切换”按钮时候,会更新父组件的内容和子组件props的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <div id="app">
    <div @click="change">切换</div>
    {{APP}}
    <hello :num="APP"></hello>
    </div>


    // 子组件
    const Hello = {
    template:'<div>{{num}}</div>',
    props:{
    num:{
    type:Number,
    default:0
    }
    }
    }

    // 父组件
    const App = {
    components:{
    Hello
    },
    setup() {
    const APP = Vue.ref(1)
    const change = () => {
    APP.value = 2
    }
    return {
    APP,
    change
    }
    },
    }

    更新组件VNode节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 新的组件vnode,涉及组件的更新策略
    // 组件数据变化可能有两种,一种是组件本身的数据变化引起,next就不存在,将之前的vnode赋值给next。另外一种是父组件更新过程中遇到子组件节点
    // 先判断子组件是否需要更新,如果需要先执行子组件的重新渲染方法,这种情况下next就是子组件的vnode
    let { next, bu, u, parent, vnode } = instance
    if (next) {
    next.el = vnode.el
    // 更新组件vnode节点信息
    updateComponentPreRender(instance, next, optimized)
    } else {
    // 没有新的组件vnode,则将之前的vnode指向next
    next = vnode
    }

    首先是更新组件的VNode。这里判断的next是什么意思呢?这里可以简单说一下,后面会详细分析。
    组件数据变化更新可能有两种:

  • 第一种是自身数据变化更新DOM,这个时候next为空

  • 第二种是父组件更新过程中遇到子节点更新,比如说,我们在父组件给子组件传递了一个props叫做num,然后我们在父组件中修改这个num的值,此时父子组件都会发生更新,第一次更新父组件时候,next就是空,执行到子组件,next就会有值。而且nextelnullnext就是下一次组件更新对应的VNode

这里vnodenext区别就是,vnode是当前的vnode,next是下一次更新的vnode
当上面的例子我们点击切换时候,会触发到effect包裹的副作用渲染函数。第一次是父组件,此时next为空,我们继续往下看。

重新生成subTree子树VNode

这里执行renderComponentRoot()方法,将我们的组件VNode(initialVNode)生成为真实的DOM VNode(subTree),然后更新我们的instance中的subTree,再缓存我们之前的subTree去用这两个新老的subTree去做patch

1
2
3
4
5
6
7
// 组件更新,重新根据组件实例生成subtree,组件里面的子组件也会生成新的subtree
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树
const prevTree = instance.subTree
// 更新子树vnode
instance.subTree = nextTree

根据新旧VNode执行patch逻辑

组件patch

组件更新时候,如果有子组件会先执行patch中的processComponent用来更新组件实例,比如Hello组件。
我们还是以App.vue中的Hello.vue组件为例,来看下更新时候的processComponent方法(这里的App.vue更新时候,实际会先执行Fragment,之后再看)

processComponent

如果我们的n1不存在就是挂载组件,否则就执行updateComponent来执行更新。

1
2
3
4
5
6
7
8
9
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
if (n1 == null) {
// 挂载组件
}
else {
// 更新子组件
updateComponent(n1, n2, optimized)
}
}

updateComponent

首先通过shouldUpdateComponent来判断我们当前的子组件实例是否需要更新,这个方法会根据props,transition,dirs等一系列属性来判断是否需要更新,如果不需要更新,那么就会简单复制属性。
如果需要更新,此时会将新的组件VNode``n2赋值给instance.next,然后重新执行update()副作用渲染函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const updateComponent = (n1, n2, optimized) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
// 新的子组件vnode赋值给instance.next
instance.next = n2
// 如果子组件已经在队列中,删除以避免重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数,也就是setupRenderEffect副作用渲染函数
instance.update()
} else {
// 无需更新,只需要复制
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}

再次重新执行update方法时候,可以看到,此时就会判断我们的next是否存在,next就是我们上面的n2,也就是新的组件vnode,此时会执行updateComponentPreRender

1
2
3
4
5
6
7
8
9
10
11
12
13
instance.update = effect(function componentEffect() {
// ......
if (next) {
next.el = vnode.el
// 更新组件vnode节点信息
updateComponentPreRender(instance, next, optimized)
} else {
// 没有新的组件vnode,则将之前的vnode指向next
next = vnode
}
// ......
})

这个方法接受一个我们当前的组件实例instance和下一次更新的组件VNode,将我们新组件VNodecomponent指向组件实例,然后组件实例的**vnode**设置为新的**vnode****,**然后更新props,slots等,因为在renderComponentRoot会使用到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在组件更新dom之前,先更新组件vnode节点信息
// 更改组件实例的vnode指针,更新props,更新插槽
// 因为在之后的renderComponentRoot中重新渲染生成子树vnode
// 依赖更新组件后的组件vnode中的props和slots数据
// instance 当前组件实例
// nextVnode 下一次渲染的组件vnode
const updateComponentPreRender = (instance,nextVNode,optimized) => {
// 更改组件实例vnode指针,新组件vnode的component指向组件实例
nextVNode.component = instance
// instance.vnode.props,是旧组件的vnode的props属性
const prevProps = instance.vnode.props
// 组件实例的vnode设置为新的vnode
instance.vnode = nextVNode
// 将next清空,为下一次做准备
instance.next = null
// 更新props
updateProps(instance, nextVNode.props, prevProps, optimized)
// 更新插槽
updateSlots(instance, nextVNode.children, optimized)

}

DOM VNodepatch

在上面重新执行完成将组件vnode生成subTree vnode之后,接下来就会执行普通元素vnodepatch流程。这里再次执行patch时候,我们的就会执行走到processElement这个流程。

processElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const patch = (
// n1是旧的vnode,n1旧节点不存在表示挂载,初次渲染为空
n1,
// n2是新的vnode,后续会根据这个vnode执行不同的处理逻辑
n2,
// DOM容器,vnode生成dom之后的挂载节点
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
// 是否采用优化模式
optimized = false
) => {
const { type, ref, shapeFlag } = n2
switch (type) {
...
default:
// 处理普通dom元素
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
...
}
}

processElement,如果是存在n1,也就是旧的VNode,那么就是更新元素,执行patchElement

1
2
3
4
5
6
7
8
9
10
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
// 挂载元素
}
else {
// 更新元素
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}

patchElement

这里主要做了两件事情,一个是更新props,也就是组件中的styleclass等。剩下就是通过patchChildren来更新子节点

1
2
3
4
5
6
7
8
9
10
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
// 更新 props
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 更新子节点
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}

patchChildren

元素的子节点可能有三种情况,纯文本、vnode数组和空,那么就会有以下几种情况:

旧节点是数组
  • 新节点都是数组-全量diff``patchKeyedChildren

  • 新节点是空-删除旧节点unmountChildren

  • 新节点是文本-删除旧节点unmountChildren然后挂载hostSetElementText

    旧节点是空
  • 新节点是文本-设置文本节点hostSetElementText

  • 新节点也是空-跳过

  • 新节点是数组,挂载mountChildren

    旧节点是纯文本
  • 新节点是纯文本,文本替换-hostSetElementText

  • 新节点是空,删除旧的节点-hostSetElementText(container, '')

  • 新节点是数组,先hostSetElementText(container, ''),再mountChildren

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized = false
    ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2
    // 子节点有三种情况,文本,数组和空
    // 如果是文本节点
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // text children fast path
    // prevShapeFlag旧节点的,
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 由数组到文本,删除之前的子节点
    unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
    // 文本对比不同,替换为新文本
    hostSetElementText(container, c2 as string)
    }{
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { //如果之前是数组节点
    // prev children was array
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { //现在也是数组节点,做全量diff
    // two arrays, cannot assume anything, do full diff
    patchKeyedChildren(
    c1 as VNode[],
    c2 as VNodeArrayChildren,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized
    )
    } else {
    // 之前是数组,现在是空节点,仅仅删除之前的子节点
    // no new children, just unmount old
    unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
    }
    } else {
    // prev children was text OR null
    // new children is array OR null
    // 之前的子节点是文本节点或者为空
    // 新的子节点是数组或者为空
    if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// 如果之前子节点是文本,则把它清空
    hostSetElementText(container, '')
    }
    // mount new if array
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// // 如果新的子节点是数组,则挂载新子节点
    mountChildren(
    c2 as VNodeArrayChildren,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized
    )
    }
    }
    }
    }

    }