packages/runtime-dom/src/index.ts 的主要作用
- 首先明确的是,在runtime-dom中调用的createApp,实际上是来自runtime-core。runtime-dom只是对它进行了类型扩展和包装导出;
- 第一步:创建渲染器
- 当createApp被调用时,他背后依赖的核心是createRenderer
ts
// 位于 runtime-dom/src/index.ts
// 1. 创建专用于DOM的渲染器,传入所有DOM操作方法(rendererOptions)
export const createApp = ((...args) => {
// 2. ensureRenderer() 会调用 createRenderer(rendererOptions),
// 生成一个具备 render 和 hydrate 方法的渲染器对象
const app = ensureRenderer().createApp(...args)
// ... 后续对 app.mount 的增强(见下文)
return app
}) as CreateAppFunction<Element>
- ensureRenderer():获取或创建唯一的DOM渲染器,他是整个应用与DOM交互的发动机。
- createRenderer(rendererOptions):这是关键。rendererOptions就是之前定义的包含patchProp、insert,createElement等几十个具体DOM操作方法的对象。createRenderer函数(来自runtime-core)接收这些具体方法,返回一个平台通用的渲染器。这是依赖注入的经典实现:核心逻辑是通用的,具体实现由外部注入。
- 第二步创建App实例(核心逻辑) 接着,调用渲染器的.createApp方法,这是在runtime-core中定义的createAppApi函数
ts
// 位于 runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
// 这个函数返回的就是我们最终使用的 createApp 函数
return function createApp(rootComponent, rootProps = null) {
// 1. 创建应用上下文对象 (context),这是全局配置和状态的集合
const context = createAppContext()
const installedPlugins = new Set() // 用于记录已安装插件,防止重复
let isMounted = false // 标记应用是否已挂载
// 2. 创建 app 对象(这就是我们得到的 app 实例)
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent, // 保存根组件对象
_props: rootProps,
_container: null, // 初始为null,mount后指向根DOM容器
_context: context, // 关联上文创建的上下文
// 3. 核心:mount 方法(初始版本,runtime-dom 会增强它)
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (isMounted) {
warn(`App has already been mounted.`) // 开发环境警告
return
}
// 3.1 标准化容器:支持字符串选择器(如'#app')或DOM元素
const container = normalizeContainer(rootContainer)
if (!container) return
// 3.2 创建根组件的虚拟节点 (vnode)
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context // 将应用上下文关联到vnode
// 3.3 核心渲染调用:将虚拟树转换为真实DOM
if (isHydrate) {
hydrate(vnode as VNode<Node, Element>, container)
} else {
render(vnode, container) // 这里调用的是 runtime-dom 提供的 render 函数
}
isMounted = true
app._container = container // 记录挂载容器
container.__vue_app__ = app // 在DOM元素上记录app实例,便于调试或HMR
// 3.4 返回根组件实例的代理(方便少数需要直接操作实例的场景)
return getExposeProxy(vnode.component!) || vnode.component!.proxy
},
// 4. 其他应用API
use(plugin: Plugin, ...options: any[]) {
// 插件安装逻辑,调用 plugin.install(app, ...options)
},
component(name: string, component?: Component): any {
// 全局组件注册,存入 context.components
},
directive(name: string, directive?: Directive) {
// 全局指令注册,存入 context.directives
},
// unmount, provide 等其他方法...
})
return app
}
}
关键行解读:
- const context = createAppContext():创建应用级别的共享上下文。所有组件树中的组件都能访问到它内部的全局组件、指令、配置等。这是app.component、app.directive全局注册的基石。
- const vnode=createVNode(...)将你传入的跟组件(可以是一个对象或者一个组件定义)转换为初始的虚拟DOM节点。Vue3的所有渲染都围绕虚拟DOM进行
- render(vnode,container):是连接所有模块的枢纽。这个render函数第一步中createRender返回的核心渲染函数。它会启动整个patch(协调)算法,递归地将vnode树比对并渲染到真实的containerDOM节点中。
- 第三步:runtime-dom对mount的增强 在
runtime-dom中,它拿到了runtime-core返回的基础app实例,但对mount方法做了关键增强,以支持更符合Web开发习惯的用法。
ts
// 位于 runtime-dom/src/index.ts (接第一步的代码)
const app = ensureRenderer().createApp(...args)
// 从 app 实例上获取原始的 mount 方法
const { mount } = app
// 重写(增强)mount 方法
app.mount = (containerOrSelector: Element | string): any => {
// 1. 标准化容器(核心增强点):支持字符串选择器
const container = normalizeContainer(containerOrSelector)
if (!container) return
// 2. 获取用户传入的根组件(可能是一个简单的模板对象)
const component = app._component
// 3. 如果根组件不是函数 & 没有render函数 & 有template,则将template编译为render函数
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML // 注意:这仅用于无构建步骤的CDN用法
}
// 4. 清除容器内的现有内容(在挂载前)
container.innerHTML = ''
// 5. 调用从 runtime-core 拿来的原始 mount 方法,执行真正的挂载
const proxy = mount(container, false, container instanceof SVGElement)
// ... 后续处理
return proxy
}
为什么需要这个增强? 用户体验:让app.mount('#app')这种写法成为可能,而不必每次都写documnet.querySelector。 支持纯HTML开发:当你在没有构建步骤(如Webpack/Vite)的环境中,直接通过 <script> 标签引入Vue时,你可能需要在HTML中写模板。这段代码会检查根组件,如果它没有 render 函数,就会尝试将 template 选项(或容器内的HTML)在运行时编译 成 render 函数。这就是"完整版"Vue(包含编译器)与"仅运行时版"的区别所在。
💎 总结
createApp 函数的主要工作可以概括为以下几步:
| 步骤 | 所在模块 | 核心工作 | 产出 |
|---|---|---|---|
| 1. 创建渲染器 | runtime-dom |
注入DOM API,创建平台专属渲染器。 | 具备 render 和 createApp 方法的渲染器对象。 |
| 2. 创建App实例 | runtime-core |
创建应用上下文和基础App对象,定义核心API。 | 一个包含 _context、基础 mount 方法的 app 实例。 |
| 3. 增强Mount | runtime-dom |
为 mount 增加容器标准化和模板编译支持。 |
最终用户使用的、功能完整的 app.mount 方法。 |
| 4. 启动应用 | 用户调用 | 用户调用 app.mount,触发虚拟DOM渲染流程。 |
根组件被渲染成真实DOM,应用启动。 |
本质上,createApp 是一个高级工厂函数,它:
- 装配环境:将平台(浏览器)的具体操作与Vue的通用核心连接。
- 创建上下文:为整个组件树建立一个共享的配置和状态空间。
- 封装启动 :提供一个简单易用的
mount方法,隐藏了虚拟DOM创建、渲染、挂载等复杂细节