在上一篇文章# Vue3源码之初始化渲染流程解读一中,主要分析了createApp()方法的具体实现。
本片文章接上一篇文章,接着分析初始化渲染mount()部分
1. mount方法定义
我们在createApp().mount()中调用的mount()方法是在导出createApp()方法内部定义的
// packages\runtime-dom\src\index.ts
/**
* @param {args} 根组件实例
*/
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// 保存app实例mount方法
const { mount } = app
// 重写实例的mount方法;
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 调用normalizeContainer获取根元素容器
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 获取根组件实例对象,就是我们在createApp()方法里面传入的根应用对象
const component = app._component
// 渲染优先级:render > template > container.innerHTML
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 挂载前清空挂载容器内部内容
container.innerHTML = ''
// 执行挂载
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
// 挂载容器添加app属性标识
container.setAttribute('data-v-app', '')
}
// 返回根组件实例
return proxy
}
// 返回应用实例以支持链式调用
return app
})
流程可以简单总结为:
- 根据mount(container)传入参数container获取挂载容器
- 获取createApp(args)传入参数args根应用对象
- 依次判断获取render > template > container.innerHTML作为渲染内容
- 挂载前清空目标容器
- 调用app应用属性方法mount挂载并添加根应用属性标识data-v-app
2. normalizeContainer 获取目标挂载容器
// packages\runtime-dom\src\index.ts
function normalizeContainer(
container: Element | ShadowRoot | string
): Element | null {
if (isString(container)) {
const res = document.querySelector(container)
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
return res
}
return container as any
}
3. 被重写mount方法
// packages\runtime-core\src\apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// appContext对象
const context = createAppContext()
// 保存当前注册plugin
const installedPlugins = new Set()
// 当前未挂载标识
let isMounted = false
/***
*
* 1. context.app = {...}
* 2. const app: App = context
*/
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
// 省略 use,mixin, component,directive,unmount,provide
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 获取vnode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// vnode渲染真实dom
render(vnode, rootContainer, isSVG)
}
// 挂载标志位
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version)
}
return vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
},
// 返回app实例
return app
}
}
mount方法内部流程总结:
- 首先根据全局挂载标识isMounted判断应用是否已经挂载,已挂载不做任何处理
- 调用createVNode方法,根据createApp(args)传入参数args创建vnode
- vnode增加appContext应用上下文变量
- 依据vnode调用render方法,虚拟dom渲染真是dom
- 修改已挂载标识
- 应用实例保存挂载容器
4. createVNode
createVnode主要用来创建vnode
vnode的本质是一个用来描述真是dom的javascript对象;vue3中通过shapeFlag对vnode进行了更详细的区分,以便在patch diff的过程中进行更精准的处理操作。
注意:有一点需要我们理解,即组件vnode其实是对抽象功能的描述,并不会在页面渲染一个真实的dom标签,真正渲染的是组件内部的html标签。
// packages\runtime-core\src\vnode.ts
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // 节点类型
props: (Data & VNodeProps) | null = null, // class style event 组件props
children: unknown = null,
patchFlag: number = 0, //
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
if (isVNode(type)) {
// 本身是vnode,直接复制
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
// 处理class style props
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 对vnode进行编码,区分vnode类型
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
// 定义vnode描述对象
const vnode: VNode = {
__v_isVNode: true, // 判断vnode标识
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
}
// 标准化子节点,将子节点处理为数组或文本节点
normalizeChildren(vnode, children)
//
return vnode
}
通过createVnode内部实现我们可以看到,创建vnode主要有:
- 首先根据type属性判断是否为vnode,是vnode则直接复制
- 处理原生属性class style
- 对vnode shapeFlag进行信息编码
- 定义vnode描述对象
- 标准化子节点
5. render
在上面createVnode方法获取到vnode之后,通过调用render执行虚拟dom渲染真实dom; render方法是createAppAPI方法的参数
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
const app: App = (context.app = {
mount() {
...
}
}
}
}
render方法真正的定义在文件:packages\runtime-core\src\renderer.ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) => {
const render: RootRenderFunction = (vnode, container, isSVG) => {
// vnode 新vdom
// container._vnode 旧vdom
if (vnode == null) {
if (container._vnode) {
// 新vnode为空,存在旧vnode情况下,卸载
unmount(container._vnode, null, null, true)
}
} else {
// 创建or更新
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// 保存vnode
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
render渲染函数内部流程说明:
- container._vnode为保存上一份 vnode
- 如果传入新vnode不存在而存在旧vnode,则执行卸载
- 根据新传入vnode和获取保留旧vnode执行新创建或者更新,初次渲染container._vnode为null,执行新建
- 更新保存container._vnode
6.patch
patch是vue3源码部分非常核心和重要的一个方法,初始化渲染以及更新diff相关全部在此方法内部定义执行,代码量也非常庞大。
// packages\runtime-core\src\renderer.ts
const patch: PatchFn = (
n1, // old vnode
n2, // new vnode
container, // 挂载容器
anchor = null, // 是一个锚点,用来标识当我们对新旧节点做增删或移动等操作时,以哪个节点为参照物
parentComponent = null, // 父组件
parentSuspense = null,
isSVG = false, // 是否svg标识
slotScopeIds = null,
optimized = false // 是否开启优化
) => {
// 旧节点存在并且与新节点类型不一致,卸载旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// PatchFlag == -2,则跳出优化模式
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 获取新vnode对象type,ref, shapeFlag
const { type, ref, shapeFlag } = n2;
// 根据 Vnode 类型判断
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
// Fragment 类型
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
// 元素类型、组件类型、teleport、supense
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})`)
}
}
}
7. processComponent
在初始化挂载阶段,我们通过createApp(App).mount('#app')在createApp传入的参数App为一个component类型,所以在第一次进入patch阶段,通过vnode.type和shapeFlag判断,会进入processComponent处理组件方法。
const processComponent = (
n1: VNode | null, // old vnode
n2: VNode, // new 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) {
// keep-live
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 初始化挂载,n1为null, 会执行此处,挂载组件方法
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 更新组件
updateComponent(n1, n2, optimized)
}
}
8. mountComponent
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例
const compatMountInstance = __COMPAT__ && initialVNode.component
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`)
}
// 设置组件实例
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// 设置并运行副作用函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
9. setupComponent
// packages\runtime-core\src\component.ts
export function setupComponent(
instance: ComponentInternalInstance, // 组件实例
isSSR = false
) {
isInSSRComponentSetup = isSSR
// 获取props children
const { props, children } = instance.vnode
//
const isStateful = isStatefulComponent(instance)
// 初始化props
initProps(instance, props, isStateful, isSSR)
// 初始化slot
initSlots(instance, children)
// 开始解析执行setup
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
10. setupStatefulComponent
// packages\runtime-core\src\component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 实例添加缓存对象
instance.accessCache = Object.create(null)
// 实例全局代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 解析获取setup函数,执行setup并获取return返回值
const { setup } = Component
if (setup) {
// setup.length获取setup方法内部参数,》1即创建传入ctx上下文对象
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
// 执行setup并拿到返回值
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
// 如果返回promise
if (isPromise(setupResult)) {
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)
}
}
11. finishComponentSetup
vue3兼容vue2 options Api,在执行完setup之后,开始兼容处理options API,这里我们就可以看出来,setup方法的执行非常靠前。
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// 实例没有render函数,即setup没有返回render函数
if (!instance.render) {
// 检查是否在options api有注册render
if (compile && !Component.render) {
// 没有render则获取template
const template =
(__COMPAT__ &&
instance.vnode.props &&
instance.vnode.props['inline-template']) ||
Component.template
if (template) {
...
...
// 执行compile编译template,并将结果保存组件render
Component.render = compile(template, finalCompilerOptions)
if (__DEV__) {
endMeasure(instance, `compile`)
}
}
}
// 实例挂载render
instance.render = (Component.render || NOOP) as InternalRenderFunction
...
...
}
12. setupRenderEffect
主要作用给实例添加update方法
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 创建响应式副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 挂载
// beforeMount hook
instance.emit('hook:beforeMount')、
// 渲染组件生成子树vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 子树vnode 挂载 container
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// mounted hook
instance.emit('hook:mounted')
// 修改挂载标志位
instance.isMounted = true
// 保留渲染生成的子树根 DOM 节点
initialVNode = container = anchor = null as any
} else {
// 更新
}
})
}
13. renderComponentRoot
方法主要执行instance实例上render方法,获取vnode;我们知道每个组件都有对应的render函数,即使我们在日常开发中使用的template模板,也都会经过compile编译返回render函数。
// packages\runtime-core\src\componentRenderUtils.ts
function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx
} = instance
let result
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 标准化vnode
result = normalizeVNode(
// 执行render
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
}
}
14. processElement
在获取到subTree子组件vnode之后,会再次进入patch方法,将子vnode挂载到父container容器,这就是一个递归渲染子组件的过程。
在递归渲染子组件过程中,遇到真实html标签,表明这是一个真是的dom元素,会在patch方法内部,根据vnode type判断执行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
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 旧节点不存在,否则直接挂载新节点
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 如果存在旧节点,则继续通过 patch 比较新旧两个节点
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
15. mountElement
在processElement内部,因为是首次渲染,n1为null,所以会到mountElement方法,挂载元素节点
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
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// 复制dom元素节点
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 创建dom元素节点
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 || !!vnode.dynamicChildren
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// 处理props class style event
if (props) {
for (const key in props) {
// 如果prop 不是key ref,则patch prop
if (!isReservedProp(key)) {
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
// 调用声明周期函数BeforeMount
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
// 挂载el到container容器
hostInsert(el, container, anchor)
}
可以看到,挂载元素函数主要做四件事:创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上。
16. mountChildren
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
// 遍历children
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
)
}
}
mountChildren方法内部比较简单,遍历每一个child,获取vnode并执行下一层patch.
17. 总结
至此,初始化渲染流程基本结束。在最后大概总结下初始化渲染流程:
- 根据mount方法内部传入参数获取到根应用挂载容器。
- 根据createApp方法传入根组件参数开始,递归构建vnode、渲染vnode、挂载。
- 组件的挂载顺序是由里到外,最开始解析的后挂载。