前言
这是vue3系列源码的第七章,使用的vue3版本是3.2.45
。
推荐
背景
在上一篇文章中,我们看了一下响应式的基本原理,详细的了解了get 时是如何触发的依赖收集,当数据发生更改的时候,set 是如何根据收集到的effect 来触发对应的更新流程的。在set 的触发流程里面,我们一直追踪,最终追踪到了componentUpdateFn这个函数。我们在渲染的时候就执行过这个函数,那么现在又到了这个函数,很明显,这个函数往下,就是页面的更新流程了。
前置
我们的页面还是和上一篇文章一样。
js
<template>
<div>{{ aa }}</div>
<div>{{ bb.name }}</div>
<div @click="change">点击</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const change = () => {
aa.value = '小识'
bb.name = '谭记'
}
const aa = ref('小石')
const bb = reactive({ name: '潭记' })
</script>
这里我们接上一篇的流程,直接到aa.value = '小识'
触发的set 流程中的componentUpdateFn函数中。
componentUpdateFn
js
const componentUpdateFn = () => {
if (!instance.isMounted) {
...
} else {
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
toggleRecurse(instance, true)
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
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)
}
}
}
这里我们走更新的这一部分流程。
这一部分的核心流程是:
const nextTree = renderComponentRoot(instance)
renderComponentRoot
这个函数其实都挺熟的了,我们在页面到底是从什么时候开始渲染的和ref reactive是怎么实现的两篇文章里面,都提到过这个函数。
他最主要的作用就是执行了render 函数,得到了组件的vnode。
js
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
这里我们先看一下这里涉及到的参数,其中setupState
我们发现,此时变量aa 的值已经发生了改变,这没问题,但是此时bb 的值竟然也更新了。我们明明才执行到aa.value = '小识'
,为什么bb的值也已经发生了改变?我们先放在这里,我们接着往下看。
那么既然执行了render 函数,那么必然的,会触发get 。 这个时候我们再到trackRefValue函数中看看,看看和初次渲染时候有什么不一样。
trackRefValue
js
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if ((process.env.NODE_ENV !== 'production')) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get" /* TrackOpTypes.GET */,
key: 'value'
});
}
else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
我们看一下参数ref
在响应式到底是怎么实现的文章里面我们看到了_rawValue
和_value
的值的更新,所以这里的值是已经更新后的值。
这里的dep 字段也不再是空的,存的是activeEffect对象, 正是通过这个对象,我们的set流程才最终触发了页面更新的过程。
然后到了trackEffects函数,这里和初次渲染不同,少了核心的依赖收集的过程。
js
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
}
else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
}
}
}
这里的shouldTrack 最终得到的是false
,没有今天依赖收集的部分。
那么get部分就结束了。
render 函数执行完了之后,就要更新<div>{{ aa }}</div>
这一部分的vnode 了,render 执行的结果作为参数传入createBaseVNode函数中。
接下来会执行剩下代码的render函数,这里的值已经发生变化了。
js
<div>{{ bb.name }}</div>
<div @click="change">点击</div>
patch
在通过renderComponentRoot(instance) 得到了更新内容的vnode之后,接下来就是patch环节了。
js
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
const { type, ref, shapeFlag } = n2
switch (type) {
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
}
}
这里看一下参数:
- n1,数据更改之前App.vue中代码段的vnode
- n2,数据更新之后App.vue中代码段的vnode
- container,
#app
Dom对象 - parentComponent,App.vue的组件实例
这里因为是代码段,所以最终执行了processFragment
processFragment
js
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (n1 == null) {
...
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
)
}
}
patchBlockChildren
js
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// Determine the container (parent element) for the patch.
const container =
// oldVNode may be an errored async setup() component inside Suspense
// which will not have a mounted element
oldVNode.el &&
// - In the case of a Fragment, we need to provide the actual parent
// of the Fragment itself so it can move its children.
(oldVNode.type === Fragment ||
// - In the case of different nodes, there is going to be a replacement
// which also requires the correct parent container
!isSameVNodeType(oldVNode, newVNode) ||
// - In the case of a component, it could contain anything.
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)!
: // In other cases, the parent container is not actually used so we
// just pass the block element here to avoid a DOM parentNode call.
fallbackContainer
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
true
)
}
}
先看一下参数:
- oldChildren,
小石
和潭记
所在的两个div组成的数组,也就是旧的vnode - newChildren,
小识
和谭记
所在的两个div组成的数组,也就是新的vnode
这里我们看到,页面上明明有三个div,为什么这里只有两个,点击的那个div不见了。
我们看一下传进来的参数:
js
patchBlockChildren( n1.dynamicChildren, dynamicChildren,
container, parentComponent, parentSuspense, isSVG, slotScopeIds )
这里传进来进行patch 的都是dynamicChildren
,固定的元素不会被更新。
这个函数就是把新旧node依次拿出来进行对比。
这个过程和mount的过程很像,从fragment --> Element
然后在patchElement的时候,最终执行
js
hostSetElementText(el, n2.children as string)
setElementText: (el, text) => {
el.textContent = text
}
对页面进行更新,此时,页面上的小石
就会变成小识
接着又依次取出一个新老节点进行对比,这次更新了潭记
为谭记
异步更新
到了这里,我们其实也把页面的更新流程大概讲了一遍,那么还记得我们文中提到的问题,为什么我们明明走在aa.value = '小识'
的set 里面,但是我们走到更新流程的时候,发现bb.name
的数据也更新了,难道bb.name = bb.name = '谭记'
这一句也执行了吗。
那自然是肯定执行了的,那么到底是什么时候,趁我们不注意执行的。
在上一篇文章,响应式到底是怎么实现的中,我们在set的流程里面,最终走到了把更新任务加入任务队列这一步。
最终调用了这个函数。
js
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
这个函数是通过promise微任务来实现异步更新的。
那么我们问题的答案也就在这里。
我们这里面是打断点一步一步调试的,然后断点直接进入了这个then函数执行。
那么这里有一个特性,如果打断点进入then 函数,那么就会直接进入then里回调函数的执行,也就是说中间的任务会偷偷执行,断点进不去。
所以事情的真相 就是,我们在aa
的数据更新了之后,我们断点进入了then 函数的回调,进入了微任务的执行,但是微任务的执行之前会先把所有的宏任务都执行完,所以在我们断点没有触及到的地方,bb.name
的数据也更新了。然后我们到后面的流程去看的时候,就发现了上面的问题。
那么应该如何避免这个问题,很简单,只要我们跳过currentFlushPromise = resolvedPromise.then(flushJobs)
这一句的执行。
你可以直接跳过queueFlush
函数的执行或者在queueFlush
函数里面执行到这一句之前直接跳出这个函数,这个时候,我们就能进入到bb.name = bb.name = '谭记'
这一句代码的执行,然后进入对应的set过程。
总结
那么通过这一个简单的例子,我们总结一下vue3页面的更新流程:
- 触发set,查找对应的更新函数
- 将更新的函数加入任务队列,作为微任务进行异步更新
- 继续触发其他的set过程,同上
- 最后执行微任务,对相应的vnode进行逐级patch,直到更新页面
这里,我们还发现了断点调试中在面对promise时候的注意事项。
那么,以上就是页面更新流程的全部内容了。