上文,我们分析了const app = createApp(App)
创建Vue实例的过程,本次接着分析app.mount("#app")
实例挂载的过程
整体流程
当执行 app.mount('#app')
时,主要经过以下步骤:
- 创建应用实例 (createApp)
- 重写 mount 方法
- 执行挂载操作
详细分析
1. 创建应用实例
首先通过 createApp
创建 Vue 应用实例(上一篇文章写了详细过程):
js
export const createApp = ((...args) => {
// 确保渲染器存在并创建应用实例
const app = ensureRenderer().createApp(...args)
// 开发环境下注入标签检查和编译选项检查
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
// ... mount 方法重写
}) as CreateAppFunction<Element>
2. mount 方法重写
在 createApp 中会重写原始的 mount 方法:
js
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 1. 规范化容器
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 2. 处理组件模板
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// 3. 清空容器内容 nodeType === 1 表示一个元素节点(Element Node)
if (container.nodeType === 1) {
container.textContent = ''
}
// 4. 执行实际挂载
const proxy = mount(container, false, resolveRootNamespace(container))
// 5. 设置挂载标记
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
3. 挂载过程的关键步骤
-
容器规范化
- 如果传入的是字符串选择器,会通过
document.querySelector
获取实际 DOM 元素
- 如果传入的是字符串选择器,会通过
-
模板处理
- 如果组件没有 render 函数和 template,会使用容器的 innerHTML 作为模板
- 这种情况下会在开发环境给出警告
-
清空容器
- 挂载前会清空容器的内容
- 通过设置
textContent = ''
实现
-
实际挂载
- 调用原始 mount 方法进行挂载
- 传入容器元素和命名空间信息
-
标记处理
- 移除
v-cloak
属性 - 添加
data-v-app
属性标记已挂载
- 移除
4. 原始 mount 函数执行流程
在重写的 mount 方法中,最终会调用原始的 mount 函数。这个函数定义在 runtime-core/apiCreateApp.ts
中:
js
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
// 1. 检查容器是否已挂载其他应用
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`
)
}
// 2. 创建根组件的 VNode
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
// 3. 关联应用上下文
vnode.appContext = context
// 4. 处理命名空间
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// 5. 执行渲染
if (isHydrate && hydrate) {
// SSR 水合模式
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 客户端渲染模式
render(vnode, rootContainer, namespace)
}
// 6. 更新挂载状态
isMounted = true
app._container = rootContainer
// 为开发工具和遥测标记容器
;(rootContainer as any).__vue_app__ = app
// 7. 开发环境处理
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
// 8. 返回组件实例的公共代理
return getComponentPublicInstance(vnode.component!)
} 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)\``
)
}
}
关键步骤解析
-
挂载检查
- 检查容器是否已经挂载了其他 Vue 应用
- 开发环境下会发出警告提示需要先卸载已有应用
-
VNode 创建
- 使用
createVNode
创建根组件的虚拟节点 - 支持自定义元素 VNode (
_ceVNode
)
- 使用
-
上下文关联
- 将应用上下文 (
appContext
) 关联到根 VNode - 确保全局配置和资源可以在组件树中访问
- 将应用上下文 (
-
命名空间处理
- 特殊处理 SVG 命名空间
- 用于正确渲染 SVG 元素
-
渲染处理
-
支持两种渲染模式:
- SSR 水合 (
hydrate
):用于服务端渲染 - 客户端渲染 (
render
):常规渲染模式
- SSR 水合 (
-
-
状态维护
- 设置
isMounted
标志 - 保存容器引用
- 在容器上标记 Vue 应用实例
- 设置
-
开发工具支持
- 保存组件实例引用
- 初始化开发工具集成
-
返回值处理
- 返回根组件实例的公共代理
- 用于外部访问组件实例
5. 虚拟节点创建详解
在 mount 函数中,通过 createVNode
创建根组件的虚拟节点。这个函数定义在 runtime-core/vnode.ts
中:
js
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false,
): VNode {
// 1. 类型检查和处理
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
// 2. 处理已存在的 VNode
if (isVNode(type)) {
// 克隆现有 VNode(用于 <component :is="vnode"/> 场景)
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
// 块树优化相关处理
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag = PatchFlags.BAIL
return cloned
}
// 3. 组件类型规范化
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 4. Props 处理
if (props) {
// 处理响应式 props
props = guardReactiveProps(props)!
let { class: klass, style } = props
// class 规范化
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
// style 规范化
if (isObject(style)) {
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 5. 确定节点类型标志
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 普通元素
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE // Suspense 组件
: isTeleport(type)
? ShapeFlags.TELEPORT // Teleport 组件
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 有状态组件
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT // 函数式组件
: 0
// 6. 开发环境的性能警告
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component that was made a reactive object. This can ` +
`lead to unnecessary performance overhead...`
)
}
// 7. 创建基础 VNode
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
创建虚拟节点的关键步骤
-
类型验证与降级
- 检查节点类型的有效性
- 无效类型降级为注释节点
-
VNode 复用处理
- 处理动态组件场景
- 合并 refs 和子节点
- 维护块树优化
-
组件规范化
- 处理类组件的选项
- 兼容 2.x 的组件写法
-
Props 规范化
- 处理响应式属性
- 规范化 class 和 style
- 确保响应式对象的可变性
-
节点类型标记
- 通过位运算确定节点类型
- 支持多种组件类型:
- 普通元素
- Suspense 组件
- Teleport 组件
- 有状态组件
- 函数式组件
-
性能优化
- 检测并警告响应式组件定义
- 建议使用 markRaw 或 shallowRef 重要概念
- ShapeFlags
- 使用位运算标记节点类型
- 便于快速判断节点特征
- BlockTree优化
- 跟踪动态节点
- 提升更新性能
6. Render 函数执行流程
在 mount 函数中,创建完虚拟节点后会调用 render 函数进行渲染。render 函数定义在 runtime-core/renderer.ts
中:
js
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
// 1. 卸载逻辑
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 2. 挂载或更新逻辑
patch(container._vnode || null, vnode, container, null, null, null, namespace)
}
// 3. 缓存 vnode 引用
container._vnode = vnode
}
渲染函数的核心逻辑
-
卸载处理
- 当传入的 vnode 为 null 时,执行卸载操作
- 通过
unmount
函数清理已存在的组件实例
-
更新处理
-
调用
patch
函数处理实际的 DOM 更新 -
参数说明:
container._vnode
: 旧的虚拟节点(首次挂载为 null)vnode
: 新的虚拟节点container
: 容器元素namespace
: 命名空间(用于 SVG 等特殊元素)
-
-
状态维护
- 将新的 vnode 缓存到容器上
- 用于下次更新时的对比
Patch 函数的作用
patch 函数是渲染系统的核心,它负责:
-
首次挂载
- 创建并插入新的 DOM 元素
- 初始化组件实例
-
更新处理
- 对比新旧虚拟节点
- 执行最小化的 DOM 操作
-
类型处理
-
根据节点类型选择不同的处理策略:
- 组件节点
- 元素节点
- 文本节点
- 注释节点等
-
重要概念
- 渲染上下文
- 维护渲染相关的状态
- 提供渲染所需的工具函数
总结
本文,通过分析 app.mount('#app')
,我们知道vue会创建虚拟节点,然后调用patch函数对虚拟节点进行diff,最终生成到真实DOM。