通过本文我们将深入理解 Vue3 的初始化过程,即:
const app = createApp({}).mount('#demo')
这步发生了什么。
我们再看下我们在上篇文章中提到的 demo:
packages/vue/examples/composition/commits.html
<script src="../../dist/vue.global.js"></script>
<div id="demo">
<h1>Latest Vue.js Commits</h1>
<template v-for="branch in branches">
<input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch">
<label :for="branch">{{ branch }}</label>
</template>
<p>vuejs/vue@{{ currentBranch }}</p>
<ul>
<li v-for="{ html_url, sha, author, commit } in commits">
<a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>
- <span class="message">{{ truncate(commit.message) }}</span><br>
by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>
at <span class="date">{{ formatDate(commit.author.date) }}</span>
</li>
</ul>
</div>
<script>
const { createApp, ref, watchEffect } = Vue
const API_URL = `https://api.github.com/repos/vuejs/vue-next/commits?per_page=3&sha=`
// 截断、删除
const truncate = v => {
const newline = v.indexOf('\n')
return newline > 0 ? v.slice(0, newline) : v
}
const formatDate = v => v.replace(/T|Z/g, ' ')
// createApp 调用的是 runtime-dom/src/index.ts#createApp ,
// 其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp
const app =
createApp({
setup() {
const currentBranch = ref('master')
const commits = ref(null)
watchEffect(() => {
fetch(`${API_URL}${currentBranch.value}`)
.then(res => res.json())
.then(data => {
console.log(data)
commits.value = data
})
})
return {
branches: ['master', 'sync'],
currentBranch,
commits,
truncate,
formatDate
}
}
});
debugger
// app.mount 调用的是 runtime-dom/src/index.ts#mount ,
// 其封装了 packages/runtime-core/src/apiCreateApp.ts#mount
app.mount('#demo')
</script>
<style>
/*
省略 css 代码
*/
</style>
1. createApp
首先看下 createApp 做了什么。
createApp 调用的是 runtime-dom/src/index.ts#createApp ,其封装了 packages/runtime-core/src/apiCreateApp.ts#createApp。我们依次看下这两个方法。
// packages/runtime-dom/src/index.ts
const rendererOptions = {
patchProp, // 处理 props 属性
...nodeOps // 处理 DOM 节点操作
}
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer | HydrationRenderer
let enabledHydration = false
// ensureRenderer 封装了 packages/runtime-core/src/renderer.ts#createRenderer
// renderer.ts#createRenderer 又封装了 baseCreateRenderer
// baseCreateRenderer 我们后面会讲
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
// 封装 createApp
export const createApp = ((...args) => {
// app 就是上面 createApp 返回的应用对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 封装 mount
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// normalizeContainer 获取选择器对应的 dom 元素
const container = normalizeContainer(containerOrSelector)
if (!container) return
// app._component 就是传给 createApp 的 options
const component = app._component
// 传给 createApp 的 options 不包含 render 和 template,则获取 container.innerHTML
// 用于后续编译成 render
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
// 首先清空我们挂载容器的 innerHTML
container.innerHTML = ''
// component.render 不存在的话, mount 阶段会编译 component.template 为 render
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
从上面的分析可以看出,createApp 其实是封装了 baseCreateRenderer 方法返回的 createApp 方法,我们接下来看下这个 baseCreateRenderer 方法。
2. baseCreateRenderer
我们看下上面提到的 baseCreateRenderer,baseCreateRenderer 这个方法足足有差不多 2000 行,baseCreateRenderer 方法里面还定义了很多方法,为了便于阅读进行了删减。
在初始化过程中,我们只需要关注 baseCreateRenderer 方法返回了什么内容即可,方法内定义的 patch 、mountElement 等方法可以以后再回来看解释。你可以直接切换到这块代码的最后查看 baseCreateRenderer 的返回值。
// packages/runtime-core/src/renderer.ts
/**
baseCreateRenderer 中定义了 patch、processElement 等一些列方法,
用于完成节点的 diff、mount(挂载)、unmount(卸载)等逻辑,我们稍后再看这些方法的具体实现。
从 baseCreateRenderer 的返回值可以看到,ensureRenderer() 的返回值就是 createAppAPI(render, hydrate) 的结果
我们接下来再看看 createAppAPI 的实现
*/
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 代码太多,省略一部分...
// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
// Patch 方法中进行节点的挂载、更新、卸载
const patch: PatchFn = (
n1, // 旧的 VNode
n2, // 新的 VNode
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) => {
// 如果有旧VNode,且新旧节点不一样,umount销毁旧节点
// 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
// 先通过节点类型 type 来判断选择处理方法
// 首次挂载时 n1 = null ,n2 是根 vnode ,代表 rootComponent
// 所以会走 processComponent 逻辑
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 逻辑,将 n2 渲染到 #demo 节点下
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__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// ...
}
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) {
// 旧的节点 n1 不存在时,就走 mount(挂载) 逻辑
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 新旧节点 n2 n1 都存在,则执行更新逻辑
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
// ...
// 处理组件挂载与更新的逻辑
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) {
// 如果是 keep-alive 组件
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 首次挂载 n1 = null, n2 = 根 vnode
// 执行 mountComponent
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 如果n1 n2 都有则执行更新
updateComponent(n1, n2, optimized)
}
}
/**
可见,在mountComponent中主要做了三件事
createComponentInstance,初始化组件实例,组件实例包括appContext、parent、root、props、attrs、slots、refs等属性
setupComponent,完善instance,
调用initProps、initSlots,初始化instance相关属性,
外还会通过setupStatefulComponent调用传入的setup方法,获取返回值setupResult,根据其数据类型
finishComponentSetup,
检测instance.render是否存在,不存在则调用compile(Component.template)编译渲染函数
在__FEATURE_OPTIONS__配置下调用applyOptions兼容Vue2.x,合并配置项到vue组件实例,初始化watch、computed、methods等配置项,调用相关生命周期钩子等
setupRenderEffect,主要是实现instance.update方法,该方法等价于effect(function componentEffect(){...}),程序如何渲染和更新视图就在这里,这也是接下来阅读的重点
*/
const mountComponent: MountComponentFn = (
initialVNode, // 初始VNode 也就是App组件生成的VNode
container, // #app Dom容器
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 创建组件实例
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// resolve props and slots for setup context
// 设置 instance 实例,初始化 props,slots 还有Vue3新增的composition API
// 如果 instance.render 属性不存在,则编译 template 获得 render 并赋给 instance.render
setupComponent(instance)
// setup() is async. This component relies on async logic to be resolved
// before proceeding
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
)
}
/**
instance 当前 vm 实例
initialVNode 可以是组件 VNode 或者普通 VNode
container 挂载的容器,例如 #demo 对应的节点
anchor, parentSuspense, isSVG 普通情况下都为 null
*/
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// create reactive effect for rendering
// 创建响应式的副作用render函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// beforeMount hook
// bm 生命周期 及 hook 执行
if (bm) {
invokeArrayFns(bm)
}
// onVnodeBeforeMount
if ((vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode)
}
// render
// ...
const subTree = (instance.subTree = renderComponentRoot(instance))
// ...
if (el && hydrateNode) {
// ...
// vnode has adopted host node - perform hydration instead of mount.
hydrateNode(
initialVNode.el as Node,
subTree,
instance,
parentSuspense,
null
)
// ...
} else {
// ...
// 把 subTree 挂载到Dom容器中
// subTree 是什么? 例如最开始的例子 App 组件为 initialVNode,subTree 就是 App组件模版里的结构生成的VNode,
// children 属性为 HelloWorld 组件VNode, p 标签VNode。
// 而App组件 initialVNode 的 chidren 里面,根据 HelloWorld 标签生成的 VNode,
// 对于 HelloWorld 组件内部DOM结构来说就是 initialVNode,而其内部DOM结构生成的VNode就是 subTree。
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// ...
initialVNode.el = subTree.el
}
// mounted hook
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
// onVnodeMounted
if ((vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
}, parentSuspense)
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
const { a } = instance
if (
a &&
initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
) {
queuePostRenderEffect(a, parentSuspense)
}
instance.isMounted = true
// #2458: deference mount-only object parameters to prevent memleaks
initialVNode = container = anchor = null as any
} 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
}
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
// render
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)
}
// updated hook
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, next!, vnode)
}, parentSuspense)
}
}
/**
createDevEffectOptions(instance) 用于后续的派发更新,它会返回一个对象:
{
scheduler: queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
},
onTrack: instance.rtc ? e => invokeHooks(instance.rtc, e) : void 0,
onTrigger: instance.rtg ? e => invokeHooks(instance.rtg, e) : void 0
}
*/
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
}
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
// 调用updateProps、updateSlots等方法更新相关数据
updateProps(instance, nextVNode.props, prevProps, optimized)
updateSlots(instance, nextVNode.children, optimized)
pauseTracking()
// props update may have triggered pre-flush watchers.
// flush them before the render update.
flushPreFlushCbs(undefined, instance.update)
resetTracking()
}
const removeFragment = (cur: RendererNode, end: RendererNode) => {
// For fragments, directly remove all contained DOM nodes.
// (fragment child nodes cannot have transition)
let next
while (cur !== end) {
next = hostNextSibling(cur)!
hostRemove(cur)
cur = next
}
hostRemove(end)
}
// container 就是我们要挂载的那个元素,即 rootContainer
// vnode 是使用根组件创建的根 vnode
const render: RootRenderFunction = (vnode, container, isSVG) => {
// unmount 逻辑
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
// patch 逻辑,首次挂载会走这里
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
// ...
return {
render,
hydrate, // 用于SSR渲染
createApp: createAppAPI(render, hydrate)
}
}
可以看到,createApp 方法是调用 createAppAPI(render, hydrate) 返回的,render 是在 baseCreateRenderer 中定义的,hydrate 用于SSR渲染,这里先不管。
我们来看一下 createAppAPI 方法。
3.createAppAPI
// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
// ...
/**
createAppAPI 工厂函数接收 render ,返回真正的 createApp 方法.
其中 render 就是上面提到的 baseCreateRenderer 中定义的 render 方法
app 就是实际返回的 app 对象,app 对象上定义的 use、mixin、component
等方法大多会返回 app 对象,因此这些方法支持链式调用,我们会在后面的文章中详细介绍这些方法,
现在我们先继续看 Vue3 的初始化过程
**/
const app: App = (context.app = {
// 其他 app 实例方法...
// rootContainer 就是我们要挂载的那个元素,比如 id 为 demo 的节点
// app.mount('#demo') 调用的就是这里的 mount 方法,执行挂载逻辑
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
// mount 方法的具体实现,下面有讲
},
})
return app
}
}
从上面的代码中我们可以看出工厂函数 createAppAPI 返回了 createApp 方法, createApp 方法最后返回了 app 对象,然后我们调用 app.mount 方法挂载节点。