前言
本文属于笔者Vue3源码阅读系列第六篇文章,往期精彩:
- 生成
vnode
到渲染vnode
的过程是怎样的 - 组件创建及其初始化过程
- 响应式实现——reactive篇
- 响应式是如何实现的(ref + ReactiveEffect篇)
- 响应式是如何实现的(track + trigger篇)
本文主要内容为组件更新的大致流程(不会涉及到很具体的更新细节,比如diff算法)。废话不多说,接下来咱们就走进正文。
组件更新流程
同样的,还是以之前的例子来分析:
// Main.vue
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const visible = ref(true)
const count = ref(0)
function onClick() {
visible.value = !visible.value
}
</script>
<template>
<div>
<div v-if="visible">{{ count }}</div>
<button @click="onClick">click</button>
<Child :visible="visible"></Child>
</div>
</template>
// Child.vue
<template>
<div>{{ props.visible }}</div>
</template>
<script setup>
const props = defineProps(['visible'])
</script>
初始化流程,依赖收集,以及trigger
副作用函数重新执行重新收集依赖在之前的文章中都已详细分析过,本文的重点在于副作用函数执行的过程,而这就恰好是组件更新过程。
当点击了按钮,执行 visible.value = !visible.value
,然后执行 ref
实例的 set
方法,通过 hasChanged
判断新旧值是否相等,如果值变化了就触发 triggerRefValue
。
从上面几篇文章我们知道接下来就触发Main.vue
对应的effect
对象的run
,在run
中执行了组件对应的更新逻辑:
run() {
// ....
// 组件更新的逻辑
return this.fn()
//....
}
那这个 this.fn
是什么呢?
这个在初始化流程中写到过,在初始化过程中,调用setupRenderEffect
:
所以执行this.fn
就相当于在执行这个componentUpdateFn
:
componentUpdateFn
const componentUpdateFn = () => {
// 判断组件是否挂载
if (!instance.isMounted) {
// 初始化执行的逻辑
} else {
// 组件更新的逻辑,这里会有两种情况:
// 1. 组件自身的状态变化,触发了组件更新,此时 next 为 null
// 2. 父组件的更新,调用 processComponent 触发了子组件的更新,此时 next 为 VNode
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
// 省略一些非主要的逻辑...
// Disallow component effect recursion during pre-lifecycle hooks.
toggleRecurse(instance, false)
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// beforeUpdate钩子逻辑省略...
toggleRecurse(instance, true)
// 省略 render 函数执行时间统计逻辑
// 执行render得到新的组件subTree
const nextTree = renderComponentRoot(instance)
// 缓存旧的组件subTree
const prevTree = instance.subTree
// 更新组件subTree
instance.subTree = nextTree
// 省略 patch 过程时间统计逻辑
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
)
// 当组件自身状态触发组件更新时,
next.el = nextTree.el
if (originNext === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
// to child component's vnode
updateHOCHostEl(instance, nextTree.el)
}
// 省略 updated 钩子的逻辑
}
}
初始化执行的逻辑在初始化流程中也分析过,本文主要关注else
分支——组件更新的逻辑,大致逻辑如下:
-
先从当前组件实例上获取
next、beforeUpdate、updated、parent、vnode
。 -
值得注意的是
next
属性用于区分组件的更新是如何触发的。触发组件更新可能会有两种情况: a. 组件自身的状态变化,触发了组件更新,此时next
为null
,如果是这种情况的话,直接把当前的vnode
赋值给next
。b. 父组件的更新,调用
processComponent
触发了子组件的更新,此时next
为 子组件新的vnode
,新的vnode
还没有el
,此时先把旧vnode.el
赋值给新vnode.el
,然后调用updateComponentPreRender
:const updateComponentPreRender = ( instance: ComponentInternalInstance, nextVNode: VNode, optimized: boolean ) => { // 设置新vnode的组件实例 nextVNode.component = instance // 得到旧的props const prevProps = instance.vnode.props // 更新组件vnode为新的 instance.vnode = nextVNode // 将next设置为null instance.next = null // 更新组件的props updateProps(instance, nextVNode.props, prevProps, optimized) // 更新组件的slots updateSlots(instance, nextVNode.children, optimized) // 设置暂停track pauseTracking() // props更新可能会触发一些watcher,比如watch、computed,在执行render之前,先执行这些副作用 flushPreFlushCbs() // 设置恢复track resetTracking() }
-
然后就是执行
beforeUpdate
钩子,如果是开发环境还会对执行render、patch
计时,用于vue-devtools
。 -
执行
render
得到新的subTree
。 -
基于新旧
subTree
执行patch
,开始更新DOM。 -
更新
next.el
为subTree.el
。 -
然后就是执行
updated
钩子。
接下来看下本例patch
的过程:
patch
在组件的初始化流程中,也是通过调用 patch
挂载 DOM
,只不过初始化时没有旧的 subTree
。
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// 如果有旧的vnode,并且新旧vnode不是同一种type,直接卸载旧vnode
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 禁用优化patch
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 根据新vnode节点的 type 和 shapeFlag 确定调用具体的处理逻辑,比如处理element节点、component、文本等
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
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
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
对于本例,调用 patch
,由于根节点是 element
,会调用 processElement
:
然后 processElement
中根据判断有没有 旧vnode
决定调用 mountElement/patchElement
:
const processElement = (//...) => {
//...
if (n1 == null) {
mountElement(//...)
} else {
patchElement(//...)
}
}
那么接下来就走到 patchElement
:
patchElement
n2.el
可能为null
,先赋值为n1.el
- 确定
patchFlag
- 拿到新旧
props
- 调用
onVnodeBeforeUpdate hook
- 如果是
dev
,并且是热更新触发,那就强制全量diff
(class
、style
、props
),不采用优化的patch
方案 dynamicChildren
(动态节点)只有使用template
开发时会生成,手写的渲染函数不行,在compiler
将template
转为render
的过程中,compiler
会生成一些辅助的信息用于优化组件更新优化时根据辅助信息,只处理可能会变化的项。比如非优化模式组件更新时我们可能要遍历处理每一个节点,并且处理每一个节点class
的变化、style
的变化、props
的变化等。但是有了辅助信息告诉我们哪些节点会变化、是节点的class
变?还是style
变?还是props
变?就只处理可能会变的项,这样一来,大大加快了patch
的速度。判断如果有dynamicChildren
,那就调用patchBlockChildren
处理子节点;否则就调用patchChildren
处理子节点。- 等子节点处理完了,再来处理该节点的
class
、style
、props
。 - 如果
patchFlag
的存在意味着该节点的render
是由编译器生成的(也就是用户是通过template
开发),可以采用优化的diff
。在这个分支中,旧节点和新节点保证具有相同的形状(即在源模板中的完全相同的位置)。- 如果
patchFlag
> 0, 再判断patchFlag
&PatchFlags.FULL_PROPS
(当节点存在动态key
的prop
时,就需要全量的diff
),否则再分别判断patchFlag
&PatchFlags.CLASS
、patchFlag
&PatchFlags.STYLE
、patchFlag
&PatchFlags.PROPS
,执行相应的操作。最后还会根据patchFlag
&PatchFlags.TEXT
执行文本更新操作。 - 如果
!optimized
&&dynamicChildren == null
,那就直接执行和patchFlag
&PatchFlags.FULL_PROPS
相同的操作。 - 从代码中不难看出,
FULL_PROPS
和CLASS
、STYLE
、PROPS
是互斥的,因为FULL_PROPS
的patch
,包括了CLASS
、STYLE
、PROPS
。
- 如果
- 调用
onVnodeUpdated
hook
。
为了好理解,放上一个表格( FULL_PROPS
、 CLASS
、 STYLE
、 PROPS
场景以及对应的 patchFlag
值):
PatchFlags | patchFlag | template |
---|---|---|
TEXT(1) | 1 | <div >{{ count }}</div> |
CLASS(2) | 2 | <div :class="cls">text</div> |
CLASS(2) | 3 = 1 + 2 | <div :class="cls">{{ count }}</div> |
STYLE(4) | 4 | <div :style="sty">text</div> |
STYLE(4) | 5 = 1 + 4 | <div :style="sty">{{ count }}</div> |
PROPS(8) | 15 = 2 + 4 + 8 + 1 | <div :class="cls" :style="sty" :value="visible">{{ count }}</div> |
FULL_PROPS(16) | 16 | <div :[key]="visible">text</div> |
FULL_PROPS(16) | 17 | <div :class="cls" :style="sty" :[key]="visible">{{ count }}</div> |
下图是一张compiler
将template
生成render
,并识别patchFlag
的例子:
接着咱们分析一下如何实现 CLASS、STYLE、PROPS
的patch
,他们都是调用hostPatchProp
:
PatchFlags | |
---|---|
CLASS | hostPatchProp(el, 'class', null, newProps.class, isSVG) |
STYLE | hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG) |
PROPS | hostPatchProp(el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren) |
hostPatchProp
在patchProp
中,根据key
的不同调用不同的方法处理,比如:
key
为class
调用patchClass
key
为style
调用patchStyle
key
为onXxx
调用patchEvent
- 其他调用
patchDOMProp
或patchAttr
看下patchClass、patchStyle、patchEvent
:
再来看下FULL_PROPS
调用的 patchProps
:
patchProps
根据newProps、oldProps
,先去掉oldProps
中包含,但是newProps
中不包含,并且不在isReservedProp中的key
;
export const isReservedProp = /*#__PURE__*/ makeMap(
// the leading comma is intentional so empty string "" is also included
',key,ref,ref_for,ref_key,' +
'onVnodeBeforeMount,onVnodeMounted,' +
'onVnodeBeforeUpdate,onVnodeUpdated,' +
'onVnodeBeforeUnmount,onVnodeUnmounted'
)
然后以newProps
为准,遍历newProps
更新prop
。
对于本例,dynamicChildren
中会有两个节点:
<div v-if="visible">{{ count }}</div>
<Child :visible="visible"></Child>
那么就会调用patchBlockChildren
:
patchBlockChildren
patchBlockChildren
中遍历children
,然后确定vnode
节点的DOM
,调用patch
。
第一个节点新旧vnode
如下,新的vnode
中是一个注释节点,旧的是一个文本节点:
由于节点类型不一致,调用patch
,会先调用unmount
将旧的节点DOM移除,然后调用 hostInsert
生成新的注释节点插入到container
,接着就是Child
组件的更新,Props
发生变化,调用patch -> processComponent -> updateComponent
:
updateComponent
在updateComponent
中:
- 如果组件是异步的并且没有加载完成,那就只需要更新组件的
props、slots
。 - 设置
instance.next = n2
,用于识别是父组件更新引起的子组件更新,然后调用instance.update()
,然后就又回到componentUpdateFn
啦,和初始化时一样,更新也是基于组件的subTree
进行递归调用patch
的过程。
总结
本文主要通过一个示例来分析组件更新的大致流程,如下图所示:
这是笔者第六篇源码分析类的文章,如果掘友们有什么建议,或者文中有错误,还请评论指出,谢谢!
如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞
】都是我创作的最大动力 ^_^
。