根组件挂载的章节我们说到创建了根组件vnode后,我们去执行patch操作,挂载组件,下面我们就进入到patch,看看究竟干了什么
patch
// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1, // 旧vnode
n2, // 新vnode
container, // 容器 container._vnode === n1
anchor = null, // 锚点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
// 如果新旧节点类型不同,卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 使用手动编写的渲染函数,应始终完全进行差分 不使用dynamicChildren
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
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) {
// element类型
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// component类型
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
...
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
...
}
...
}
进入patch,会对type进行一个判断,根绝类型来决定对应的处理,我们这里传入的是根组件的vnode,所以type就是根组件的组件描述对象,所以会执行processComponent函数。
processComponent
// packages/runtime-core/src/renderer.ts
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
// 判断旧节点是否为null 为null则执行挂载操作 否则执行更新操作
if (n1 == null) {
// 是否缓存
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
mountComponent
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x 兼容vue2
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 创建组件实例 createComponentInstance执行
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
...
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 初始化组件 处理setup的两个参数 执行setup 生成render函数
//(所以setup是在所有选项式API钩子之前调用 包括beforeCreate)
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
...
// 1 创建一个组件更新函数
// 1.1 render获得vnode
// 1.2 patch(oldVnode, newVnode)
// 2 创建更新机制 new ReactiveEffect(更新函数)
// 执行渲染副作用函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
进入mountComponent后首先会创建组件实例,然后去执行setupComponent,初始化一些数据,再下面就是创建函数的更新机制,这里是组件能响应式更新的核心。我们先看setupComponent做了什么:
setupComponent
// packages/runtime-core/src/component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
// vnode的props和子元素
const { props, children } = instance.vnode
// 是否是有状态的组件
const isStateful = isStatefulComponent(instance)
// 初始化props
initProps(instance, props, isStateful, isSSR)
// 初始化slots
initSlots(instance, children)
// 执行setupStatefulComponent获取setupResult
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR) // 执行setup
: undefined
isInSSRComponentSetup = false
return setupResult
}
可以看到setupComponent会初始化props和slots,然后执行setupStatefulComponent,这里主要是执行setup函数,并返回结果
setupStatefulComponent
// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 组件描述对象
const Component = instance.type as ComponentOptions
...
// 0. create render proxy property access cache
// 创建render proxy属性访问缓存 作用是缓存访问过的属性
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 代理ctx,拦截ctx的属性访问 从而实现取值的优先级:setupState > data > props > ctx
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
const { setup } = Component
if (setup) {
// 创建setupContext
const setupContext = (instance.setupContext =
// setup参数个数判断 大于一个参数创建setupContext
setup.length > 1 ? createSetupContext(instance) : null)
// instance赋值给currentInstance
// 设置当前实例为instance 为了在setup中可以通过getCurrentInstance获取到当前实例
// 同时开启instance.scope.on()
setCurrentInstance(instance)
// 暂停tracking 暂停收集副作用函数
pauseTracking()
// 执行setup
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
// setup参数
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// 重新开启副作用收集
resetTracking()
// currentInstance置为空
// activeEffectScope赋值为instance.scope.parent
// 同时instance.scope.off()
unsetCurrentInstance()
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
...
} else if (__FEATURE_SUSPENSE__) {
...
} else if (__DEV__) {
...
}
} else {
// 处理setupResult 如果为函数则会作用组件的render函数 否则赋值给instance.setupState
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 处理options API 兼容vue2
finishComponentSetup(instance, isSSR)
}
}
上述函数主要是执行setup函数,还会执行finishComponentSetup,处理options API,
其实从这里我们还能看出setup和其他options生命周期的执行顺序,先执行的setup,然后才去处理options钩子函数。
处理完setup,就该去执行setupRenderEffect了
setupRenderEffect
// packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 组件挂载/更新函数
const componentUpdateFn = () => {
...
}
// create reactive effect for rendering
// 创建effect
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // scheduler
instance.scope // 将effect添加到组件的scope.effects中
))
// 组件更新函数
const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
if (__DEV__) {
...
}
// 首次执行组件更新
update()
}
该函数只做了一件事,创建了一个effect,然后执行update(),最终会执行componentUpdateFn函数。componentUpdateFn中会执行render函数和patch操作,执行render函数的过程中会建立响应式数据和effect的关系,以后只要响应式数据发生变化就会再次执行componentUpdateFn函数已达到组件更新。下面具体看看componentUpdateFn做了什么。PS:这里不熟悉响应式的小伙伴可能会有点懵,但是不要着急,以后章节会讲到底怎么实现的响应式,响应式数据到底怎么收集到的effect对象。
componentUpdateFn
为了方便阅读只放了比较核心的代码:
const componentUpdateFn = () => {
// 判断组件是否已经挂载
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
// 生命周期和父instance
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
...
if (el && hydrateNode) {
// ssr 相关
} else {
if (__DEV__) {
startMeasure(instance, `render`)
}
// 执行render函数获得subTree(也是一个vnode) 讲subTree挂载到instance上 以供更新使用
const subTree = (instance.subTree = renderComponentRoot(instance))
...
// patch subTree初次挂载
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
if (__DEV__) {
endMeasure(instance, `patch`)
}
// el同步到initialVNode
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
...
// 组件已经挂载
instance.isMounted = true
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentAdded(instance)
}
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} else {
// 组件更新
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
...
// 执行render函数获得nextTree
const nextTree = renderComponentRoot(instance)
if (__DEV__) {
endMeasure(instance, `render`)
}
// 获取老的subTree
const prevTree = instance.subTree
instance.subTree = nextTree
if (__DEV__) {
startMeasure(instance, `patch`)
}
// 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
)
...
}
}
这里的patch操作其实就是调用本章开头的那个patch,可以看到patch其实是一个递归操作,这里patch subtree如果根组件的根元素是组件则会继续执行processComponent,如果是一个element元素则会执行processElement,processElement中会处理children,又会调用patch,如此递归直到整个组件挂载完成。
总结
- patch(n1, n2):组件类型的vnode匹配到processComponent,处理组件的挂载或更新
- processComponent:此时组件还未挂载,所以n1为null,执行mountComponent挂载组件
- mountComponent:首先创建组件实例;然后执行setupComponent初始化组件,setupComponent中会先执行setup,然后处理options API(options API的处理顺序是:props:props在setup执行前就已经处理过了 -> inject -> methods -> data -> computed -> watch),处理options API之前会执行beforeCreate生命周期,处理完options API后会执行created生命周期;然后执行setupRenderEffect,这里就会创建一个组件更新函数componentUpdateFn,通过new ReactiveEffect传入componentUpdateFn创建出该组件的effect实例,然后执行组件更新函数,componentUpdateFn中会判断isMounted为false,然后生成subTree(这里就会通过track方法去收集对应的effect),然后patch(null, subTree),beforeMount和mounted生命周期会在这个过程前后执行。
- 父子组件更新:当某个响应式变量发生改变,trigger收集的effects被拿出来执行其scheduler或run方法,flushJobs时会对effects进行排序(父组件排到前面执行),所以先执行父组件的组件更新函数,在组件更新函数中会生成新的subtree和老的subtree进行patch操作,在path过程中遇到子组件,先判断子组件是否需要更新,这里只考虑props的情况下会进行浅层比较,如果需要更新则执行子组件的更新函数(这里会移除queue中的子组件effect防止重复执行)。这里符合深度优先的更新策略。这里值得注意的是父子updated钩子的调用顺序不是固定的:假设现在一个响应式数据的变化(这里的变化特指一个reactive对象的某个属性变化)将执行父子组件组件更新函数,执行父组件的patch到检查子组件是否需要更新时,因为是浅层比较props所以结果是true,这就意味着父组件认为子组件不需要更新,直到父组件更新完将updated放到对应的queue中,父组件的更新函数执行完后轮到子组件的更新函数执行,完成后放到queue中,这种情况下就导致父组件的updated钩子函数会先执行。除了这种情况,如果在父组件的patch过程中通过props的浅层比较判断子组件需要更新,这种情况下就是子组件的updated钩子先执行。