「这是我参与2022首次更文挑战的第22天,活动详情查看:2022首次更文挑战」
Vue3大侠修炼手册4- 初始化流程分析(2)
题记
上篇文章我们分析了createApp
的app实例构建过程,同时简要分析了mount
的流程。今天这篇文章,我们书接上文,更加深入的窥探一下mount
的流程。
再看mount
在上文中,我们在todomvc.html
中把断点放到mount
处来查看mount
的流程。 然后根据代码单步调试,我们知道了其虽然进行了封装,但最终核心部分仍是调用了 apiCreateApp.ts
文件下的 createAppAPI
函数所返回的对象中的mount
方法。 而该方法主要做了两件事
-
调用
createVNode
方法,创建vnode
-
调用
render(vnode, rootContainer, isSVG)
方法,把vnode转化为真实dom,然后绑定到rootContainer
上。
关于createVNode
部分,我们后面会专门讲解,今天这篇文章,我们着重分析,这个render
函数所做的事情。
我们从上一节代码分析可知,此处的render
函数,是在baseCreateRenderer
定义并返回的 render
函数。
render函数
这段函数不长,我把它复制下来:
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 核心执行流程
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
复制代码
我们可以看到这个函数里,由于我们上面刚更新过vnode
的值,所以函数内部走else
流程,执行了一个名为patch
的函数, 当我们进入其中,可以发现由于首次container._vnode
为空, 我们走了挂载流程,patch
函数就是我们要找的核心更新/挂载函数。
在这里我去除其中的边角逻辑,只展示核心的判断逻辑.
patch函数
源码片段:
// packages/runtime-core/src/renderer.ts 中 patch函数
// 删除了边角逻辑等,方便大家阅读,若要看全部内容,请移步源码
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) { // 根据type执行对应的流程
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
// ... mountStaticNode() or patchStaticNode()
break
case Fragment:
// ... processFragment()
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // !! 首次进入会进入processComponent
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// ...
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// ...
} else if (__DEV__) {
// ...
}
}
}
复制代码
断点截图: 通过单步断点我们可以看到首次的type
为一个对象,shapeFlag
为4
,所以会走switch
的default
流程。而通过&
位运算符的执行结果和断点执行我们可以知道,首次会执行processComponent
函数。
processComponent函数
processComponent源码片段
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
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)
}
}
复制代码
根据上面的代码我们可以知道此时n1 == null
所以我们的代码会走mountComponent
流程,是挂载流程,而非updateComponent
流程。
我们继续深入mountComponent
代码中看看里面究竟做了哪些事情
mountComponent
mountComponent精简源码
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例
const instance: ComponentInternalInstance = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
// 组件的初始化
setupComponent(instance)
}
// 执行副作用,在setupComponent时已经确保有了render函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
}
复制代码
重上图我精简后的代码我们可以看到mountComponent
主要做了三件事,分别调用了三个函数
- 创建组件实例 -
createComponentInstance
- 进行组件的初始化 -
setupComponent
- 执行组件渲染副作用函数 -
setupRenderEffect
关于第一步创建实例,我们今天先不讨论,感兴趣的同学可以自行进入调试学习。 我们主要看2、3步。
我们先来看第二步
setupComponent函数
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
// 初始化 props和slots
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
// 初始化组件
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
复制代码
我们可以看到setupComponent
函数除初始化props
和slots
以外,还执行了setupStatefulComponent
. 那我们再浏览一下这个 setupStatefulComponent
函数
setupStatefulComponent
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
// 2. call setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
unsetCurrentInstance()
handleSetupResult(instance, setupResult, isSSR)
} else {
finishComponentSetup(instance, isSSR)
}
}
复制代码
我们不去看目前我们不关系的逻辑,不然会陷入无尽的细节,而走不出我们的流程。我们主要最后这个if...else
逻辑的最后三行代码,而进入handleSetupResult
函数我们进入会发现,最终handleSetupResult
还是会调用finishComponentSetup
这个函数。
而这个函数目前来说主要的目的就是判断我们的instance
是否已经有render
函数,如果没有,会帮助其设置一个render
函数,这个render函数的功能就是把我们的模板函数转换为vnode
。在这里我们先不深究,我把精简后的代码贴到下面。
finishComponentSetup 精简代码
export function finishComponentSetup(
instance: ComponentInternalInstance
) {
const Component = instance.type as ComponentOptions
// 处理 instance 的 render
if (!instance.render) {
// only do on-the-fly compile exist
if (compile && !Component.render) {
const template = Component.template
if (template) {
Component.render = compile(template, finalCompilerOptions)
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
}
// support for 2.x options 支持 vue2
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
setCurrentInstance(instance)
pauseTracking()
applyOptions(instance) // 对除了setup之外的所有选项做处理
resetTracking()
unsetCurrentInstance()
}
}
复制代码
这样一来,在执行完第二步 进行组件的初始化setupComponent
时,我们确保了 组件的实例有了render
函数,以便在未来执行。
我们看完第二步,再看看setupRenderEffect
时都做了什么。
setupRenderEffect函数
setupRenderEffect函数精简代码
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
toggleRecurse(instance, false)
// beforeMount hook
if (bm) {
invokeArrayFns(bm)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeMount')
}
// 渲染函数的执行过程,subTree为当前组件的子树,vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// patch递归遍历vnode
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
initialVNode = container = anchor = null as any
} else {
// ... 更新流程
}
}
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
))
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
update()
}
复制代码
上面的代码,我们已经删除了和本教程无关的逻辑,以求让读者能更清晰的把控好本函数在挂载阶段所做的工作内容。从上面代码我们可以很清楚的看到,它主要创建了一个名为componentUpdateFn
的函数。然后通过ReactiveEffect
把componentUpdateFn
构建出异步函数effect
,以便在以后的特定时机进行调用。
既然创建effect
的作用是以后调用componentUpdateFn
,那我们先分析一下这个函数的作用,也就可以清晰的知道setupRenderEffect
所做的事情了。
通过上面的代码我们可以看到componentUpdateFn
主要做了一下
- 该方法会执行很多hooks
- 构建
vnode
子树 const subTree = (instance.subTree = renderComponentRoot(instance)) - 执行
patch
,递归的完成vnode的挂载和更新
通过上面的componentUpdateFn
函数的执行,我们又回到了patch
进行执行,只是这次的patch
执行的是instance.subTree
,所以这里会一直进行递归执行,直到所有需要子节点都执行完毕,此时我们的跟节点的patch
函数才算是结束执行。
看来我们已经接近胜利了,接下来我们再继续走一遍来检查我们上面的想法是否正确。我们继续执行componmentUpdateFn
函数中的patch
函数,看我们断点调试的结果
根据type
等结果,此时的section
进入了processElement
函数。
processElement函数
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
复制代码
processElement函数在挂载阶段只是调用了mountElement
函数,我们直接看mountElement
函数。
mountElement函数
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// hostCreateElement创建dom元素
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 递归children
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
}
复制代码
mountChildren函数
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
// 循环patch
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
复制代码
我们看到mountElement
函数主要做了两件事
hostCreateElement
,该操作会创建vode对应的dom元素。- 如果有子节点会执行
mountChildren
函数。而mountChildren
函数是循环调用patch
函数来执行对应child
过程。
关于挂载的流程我们就先分析这么多。整个过程还是相对来说比较复杂的。主要难点在于patch
函数的递归调用流程。 在这里我总结了一张思维导图,放在这里仅供大家参考。
结语
到目前为止,我们已经完成了对vue3
源码的初始化流程的分析。相信大家通过一步一步的调试一定收货匪浅。不过由于流程的复杂性建议小伙伴们多执行几遍,遇到不懂得地方多分析,最好把画出思维导图来帮助自己理清思路。
当然有问题的地方也可以留言互动,我们下期见~