vue3 源码学习:组件更新过程
在前一篇文章中,我介绍了 vue3 中组件初次渲染为 DOM 元素的过程,下面介绍一下当组件发生变化时,vue3 更新组件的过程
组件类型的 vnode 挂载的过程时,在 setupRenderEffect
方法中,定义了一个 componentUpdateFn
方法并通过 ReactiveEffect
转换一个副作用的渲染函数,当组件状态发生变化时,会自动触发副作用函数 componentUpdateFn
执行,下面我们来看更新时的函数执行逻辑
可以看到当组件更新时,会进入到 patch
函数中执行更新逻辑
1const componentUpdateFn = () => {
2 // 组件初次挂载
3 if (!instance.isMounted) {
4 // ...
5 }
6 // 组件更新逻辑
7 else {
8 let { next, vnode } = instance
9
10 // next 记录未渲染的父组件 vnode
11 if (next) {
12 next.el = vnode.el
13 updateComponentPreRender(instance, next, optimized)
14 } else {
15 next = vnode
16 }
17
18 // 更新子节点 node
19 const nextTree = renderComponentRoot(instance)
20 const prevTree = instance.subTree
21 instance.subTree = nextTree
22
23 // 核心:进入patch 更新流程
24 patch(/* 相关参数 */)
25
26 next.el = nextTree.el
27 }
28}
patch
函数根据不同类型的 vnode 做不同的处理操作,同样我们看核心 processElement
处理元素类型和processComponent
处理组件类型函数过程
组件类型更新过程
由于是更新组件,n1 不为 null,进入到 updateComponent
函数
1const processComponent = (/* 相关参数 */) => {
2 n2.slotScopeIds = slotScopeIds
3 if (n1 == null) {
4 // 挂载操作
5 } else {
6 // 更新组件
7 updateComponent(n1, n2, optimized)
8 }
9}
在 updateComponent
函数执行逻辑如下
- 调用
shouldUpdateComponent
方法判断是否需要更新组件,如果不用更新组件,则仅更新节点 DOM 元素缓存和 instance 的 vnode - 如果需要更新组件,将新 vnode 赋值给 instance 的 next 属性,并调用 instance 的
update
方法更新组件,update
方法就是在挂载组件时,在setupRenderEffect
方法中定义的响应式函数
1const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
2 const instance = (n2.component = n1.component)!
3 // 判断函数是否需要更新
4 if (shouldUpdateComponent(n1, n2, optimized)) {
5 instance.next = n2
6 instance.update()
7 }
8 // 无需更新仅复制元素
9 else {
10 n2.el = n1.el
11 instance.vnode = n2
12 }
13}
那么组件在什么情况下需要更新呢,shouldUpdateComponent
中定义了下面几种情况都会强制更新组件
- HMR 热更新
- 动态节点,动态节点包括含有 vue 指令的节点(v-if 等)和使用了 transition 的节点
- 优化过 vnode(即存在 patchFlag 标志位)
- 子节点是动态插槽(在 v-for 中)
- 存在完整属性标志位
PatchFlags.FULL_PROPS
,比较新旧 props 是否相同,不相同强制更新 - 存在部分属性标志位
PatchFlags.PROPS
,新旧节点的动态 props 不同则强制更新
- 手动编写
render
函数的场景 - 新 props 存在而旧 props 不存在
- 存在 emits 时,新旧 props 不同则强制更新
1export function shouldUpdateComponent(
2 prevVNode: VNode,
3 nextVNode: VNode,
4 optimized?: boolean
5): boolean {
6 const { props: prevProps, children: prevChildren, component } = prevVNode
7 const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
8 const emits = component!.emitsOptions
9
10 // 热更新场景
11 if (nextVNode.dirs || nextVNode.transition) {
12 return true
13 }
14
15 // 当存在优化标志位
16 if (optimized && patchFlag >= 0) {
17 // 子节点是动态插槽
18 if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
19 return true
20 }
21 // 存在完整属性标志位
22 if (patchFlag & PatchFlags.FULL_PROPS) {
23 if (!prevProps) {
24 return !!nextProps
25 }
26 return hasPropsChanged(prevProps, nextProps!, emits)
27 }
28 // 存在部分属性标志位
29 else if (patchFlag & PatchFlags.PROPS) {
30 const dynamicProps = nextVNode.dynamicProps!
31 for (let i = 0; i < dynamicProps.length; i++) {
32 const key = dynamicProps[i]
33 if (
34 nextProps![key] !== prevProps![key] &&
35 !isEmitListener(emits, key)
36 ) {
37 return true
38 }
39 }
40 }
41 } else {
42 // 手动编写 render 函数
43 if (prevChildren || nextChildren) {
44 if (!nextChildren || !(nextChildren as any).$stable) {
45 return true
46 }
47 }
48 // 新旧 props 相同则跳过更新
49 if (prevProps === nextProps) {
50 return false
51 }
52 if (!prevProps) {
53 return !!nextProps
54 }
55 if (!nextProps) {
56 return true
57 }
58 return hasPropsChanged(prevProps, nextProps, emits)
59 }
60
61 return false
62}
另一点需要提出的是,在 shouldComponentUpdate
结果为 true 判断需要更新时,会将新节点 vnode 赋值给 next 属性,此处 next 属性的作用是:标记接下来需要渲染的子组件,当 next 属性存在时,通过 updateComponentPreRender
更新实例上的 props、slots、vnode 信息,保证后续组件渲染取值是最新的
1const componentUpdateFn = () => {
2 // 组件初次挂载
3 if (!instance.isMounted) {
4
5 }
6 // 组件更新逻辑
7 else {
8 let { next, vnode } = instance
9
10+ // next 记录未渲染的父组件 vnode
11+ if (next) {
12+ next.el = vnode.el
13+ updateComponentPreRender(instance, next, optimized)
14 } else {
15 next = vnode
16 }
17
18 // 更新子节点 node
19 const nextTree = renderComponentRoot(instance)
20 const prevTree = instance.subTree
21 instance.subTree = nextTree
22
23 // 核心:进入patch 更新流程
24 patch( /* 相关参数 */)
25
26 next.el = nextTree.el
27 }
28}
元素更新过程
更新元素时,由于 n1 不为 null,同样进入到 patchElement
更新元素方法
1const processElement = (/* 相关参数 */) => {
2 if (n1 == null) {
3 } else {
4 patchElement(/* 相关参数 */)
5 }
6}
patchElement
方法执行过程如下,函数中的核心逻辑是进入 patchChildren
方法对子节点进行完整的 diff 处理过程
- 根据 dynamicChildren 判断是否是动态元素(比如带有 v-for 指令的元素),如果是动态元素调用
patchBlockChildren
更新动态子节点,否则调用patchChildren
方法更新 - 根据 patchFlag,依次处理 props、class、style 等属性
1const patchElement = (/* 相关参数 */) => {
2 const el = (n2.el = n1.el!)
3 let { patchFlag, dynamicChildren } = n2
4 patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
5 const oldProps = n1.props || EMPTY_OBJ
6 const newProps = n2.props || EMPTY_OBJ
7
8 // 根据 dynamicChildren 判断是否是动态元素
9 if (dynamicChildren) {
10 // 处理动态子节点
11 patchBlockChildren(/* 相关参数 */)
12 } else if (!optimized) {
13 // 全量diff处理子节点
14 patchChildren(/* 相关参数 */)
15 }
16
17 // 根据 patchFlag,依次处理 props、class、style 等属性
18 if (patchFlag > 0) {
19 if (patchFlag & PatchFlags.FULL_PROPS) {
20 // element props contain dynamic keys, full diff needed
21 patchProps(/* 相关参数 */)
22 } else {
23 if (patchFlag & PatchFlags.CLASS) {
24 if (oldProps.class !== newProps.class) {
25 hostPatchProp(el, 'class', null, newProps.class, isSVG)
26 }
27 }
28 if (patchFlag & PatchFlags.STYLE) {
29 hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
30 }
31
32 if (patchFlag & PatchFlags.PROPS) {
33 const propsToUpdate = n2.dynamicProps!
34 for (let i = 0; i < propsToUpdate.length; i++) {
35 const key = propsToUpdate[i]
36 const prev = oldProps[key]
37 const next = newProps[key]
38 // #1471 force patch value
39 if (next !== prev || key === 'value') {
40 hostPatchProp(/* 相关参数 */)
41 }
42 }
43 }
44 }
45
46 if (patchFlag & PatchFlags.TEXT) {
47 if (n1.children !== n2.children) {
48 hostSetElementText(el, n2.children as string)
49 }
50 }
51 } else if (!optimized && dynamicChildren == null) {
52 patchProps(/* 相关参数 */)
53 }
54}
在 patchChildren
中,子节点一共分为三种类型:文本类型、数组类型、空节点,数组类型即代表元素还存在子节点。根据新旧节点这三种类型的两两组合,一个会存在 9 种场景,代码中的判断逻辑相对复杂,但整理为表格之后就非常清晰明了。对于新旧节点都是数组类型,进入完成 diff 算法的计划再新写一篇文章
| | 新节点为空 | 新节点文本 | 新节点为数组 | | ------------ | ---------- | ---------------------- | ------------------------ | | 旧节点为空 | 不做操作 | 添加新文本 | 添加新子节点 | | 旧节点为文本 | 删除旧文本 | 更新文本 | 移除旧文本,添加新子节点 | | 旧节点为数组 | 删除旧数组 | 移除旧节点,添加新文本 | 完整 diff 算法 |
1const patchChildren: PatchChildrenFn = (/* 相关参数 */) => {
2 const c1 = n1 && n1.children
3 const prevShapeFlag = n1 ? n1.shapeFlag : 0
4 const c2 = n2.children
5
6 const { patchFlag, shapeFlag } = n2
7 // 如果存在 patchFlag,进行优化算法处理
8 if (patchFlag > 0) {
9 if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
10 patchKeyedChildren(/* 相关参数 */)
11 return
12 } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
13 patchUnkeyedChildren(/* 相关参数 */)
14 return
15 }
16 }
17
18 // 新旧节点三种类型 9 种组合的处理过程
19 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
20 // 新节点是文本节点,旧节点是数组或文本节点,先卸载旧节点
21 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
22 unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
23 }
24 // 新旧节点都是文本节点,更新文本内容
25 if (c2 !== c1) {
26 hostSetElementText(container, c2 as string)
27 }
28 } else {
29 if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
30 // 新旧节点都是数组,完整 diff 算法
31 if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
32 patchKeyedChildren(/* 相关参数 */)
33 } else {
34 // 新节点不是数组,旧节点是数组,卸载旧节点
35 unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
36 }
37 } else {
38 // 新节点是数组,旧节点是文本节点,先卸载旧节点
39 if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
40 hostSetElementText(container, '')
41 }
42 // 新节点是数组,挂载新节点
43 if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
44 mountChildren(/* 相关参数 */)
45 }
46 }
47 }
48}
组件更新过程总结
上面介绍了组件类型和元素类型的更新过程,下面我们举一个具体的例子来总结整体的回顾流程
父组件 App.vue
1<template>
2 <div>
3 父组件
4 <child :msg="msg" />
5 <button @click="handleUpdateMsg">更新 msg</button>
6 </div>
7</template>
8<script setup>
9 import { ref } from 'vue'
10 import Child from './child.vue'
11
12 const msg = ref('初始化')
13
14 function handleUpdateMsg() {
15 msg.value = '更新后'
16 }
17</script>
子组件 child.vue
1<template>
2 {{ msg }}
3</template>
4<script setup>
5 import { toRefs } from 'vue'
6 const props = difineProps({
7 msg: {
8 type: String
9 }
10 })
11
12 const { msg } = toRefs(props)
13</script>
在父组件 App.vue 中,内部使用了一个 child.vue 子组件,当点击按钮修改 msg 时,两个组件的更新过程如下
- 点击按钮,App 组件自身状态发生变化,进入组件更新逻辑,此时 next 属性为 null,根据新旧 subTree 进入
patch
方法 - 此时组件的类型为 div,进入
patchElement
更新元素,此时 div 节点下有 child 子组件,进入组件更新流程 - 子组件 child 进入
updateComponent
方法,shouldUpdateComponent
判断为 true 需要更新组件,将新子组件 vnode 赋值为 instance.next,调用instance.update()
方法进入副作用渲染函数 - 此时 next 属性有值,调用
updateComponentPreRender
更新实例数据,完成子组件 child 渲染过程 - 子组件更新完成后,父组件 App 根据最新的实例数据,渲染最新的子节点
最后在上次的挂载流程图上,补充组件过程过程的逻辑(蓝色部分)