目录:
一. 前言
在createApp方法执行中,我们先执行了createVNode生成了VNode,然后执行render方法,并且将VNode作为参数传入,同时将context值绑定到vnode.appContext属性上,接下来我们看下render方法的实现。
//源码路径 core/packages/runtime-core/src/apiCreateApp.ts
vnode.appContext = context
render(vnode, rootContainer, isSVG)
二. render 方法的定义
代码很简短,可以看到,如果传入的VNode为空,则直接卸载旧的dom,反则执行patch方法,从vue2中我们可以了解到,该方法是一个diff算法,将新旧dom进行比对后渲染,让我们去看下patch方法的实现。
//源码路径 core/packages/runtime-core/src/renderer.ts
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
}
三. patch 首次执行
1. patch 的调用
我们可以看到,传入了一个container._vnode和vnode,而container._vnode初始值为null,该值作为旧的节点,也就是目前真实的dom,而vnode是新的节点,意思是即将要渲染为真实dom的虚拟节点。
//源码路径 core/packages/runtime-core/src/renderer.ts
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
2. patch 的实现
该方法内部实现用来为不同的vnode做不同的处理。
- 首先,通过判断如果更改前后节点相同的话直接退出,不做任何处理
- 通过
isSameVNodeType对比新旧节点的类型,不相同直接卸载掉旧的节点 - 接下来通过
switch来对不同的type进行不同的处理,第一次挂载则进入processComponent方法,因此我们接着往下看
//源码路径 core/packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1, // 上一次 vnode 初始值null
n2, // 新的 vnode
container, // 真实dom
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
}
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) {
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})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
3. processComponent
我们看到,首先进来之后有一个判断n1是否为null,如果n1为null,则说明还没有真实dom被渲染,则进行第一次执行挂载mountComponent,反则执行updataComponent。我们继续往下看,进入mountComponent方法中。
//源码路径 core/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
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)
}
}
4.mountComponent
首先在该组件中,执行了createComponentInstance方法,创建了组件实例instance,然后执行setupComponent,也就是在这其中执行了我们传入的setup,我们接着往下看。
//源码路径 core/packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode, // 根组件
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component;
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
5. setupComponent
该方法首先对props 和 slot进行了初始化,然后执行了关键的一步setStatefulComponent,我们接着往下看。
//源码路径 core/packages/runtime-core/src/component.ts
function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
6.setStatefulComponent
该函数中首先通过setup.length来确定回调函数参数,如果有两个参数,则将执行createSetupComtext方法来构建我们的第二个参数ctx。然后通过callWithErrotHandling方法执行setup方法,判断其执行结果是否为Promise对异步组件进行处理,反则执行handleSetupResult方法,对结果进行处理。
//源码路径 core/packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 2. call setup()
const { setup } = Component
if (setup) {
// 根据setup参数
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// 异步组件处理
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
7.setupRenderEffect
在执行完 setup 之后,我们继续回到mountComponent中,执行该方法中的最后一个方法setupRenderEffect。
下面源码仅展示一部分,主要再其中定义了一个方法componentUpdateFn,当该组件中的响应式数据发生改变时,就会执行该方法更新页面。实现data 驱动 视图。我们可以看到componentUpdateFn中又执行了patch,进行递归渲染页面。我们继续往下看。
//源码路径 core/packages/runtime-core/src/renderer.ts
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)
if(el && hydrateNode){
// 服务器渲染
} else {
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
initialVNode.el = subTree.el
}
instance.isMounted = true
}
// 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.id = instance.uid
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
toggleRecurse(instance, true)
update()
}
四. patch 再次执行
上一次我们在patch中进入了processComponent中,这次,我们进入proccessElement,我们继续往下看
1. processElement
可以从下面源码中看到,通过判断n1 == null来判断是否为第一次执行,如果第一次执行则进入mountElement方法中,否则进入patchElement中,我们先来看看mountElement中发生了什么。
//源码路径 core/packages/runtime-core/src/renderer.ts
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
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
2. mountElement
下边源码省略部分实现,该方法中主要执行了两个操作,一是 hostCreateElement方法,二是hostInsert,除此之外,判断该element是否还有子节点,有的话执行mountChildren,再重复之前的方法。
hostCreateElement: 用来创建一个Element,类似于document.createELementhostInsert:用来将Element插入到页面中,类似于document.insertbefore。
//源码路径 core/packages/runtime-core/src/renderer.ts
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
}
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
hostInsert(el, container, anchor)
五. 总结
整个render渲染流程大致如下:\