Vue3 createApp 创建应用实例
createApp 解决了什么问题
Vue2 用 new Vue() 创建根实例,所有的插件、全局组件、全局指令、原型扩展都挂在 Vue 这个全局构造函数上:
// Vue 2
Vue.use(VueRouter);
Vue.component('MyButton', MyButton);
Vue.prototype.$http = axios;
new Vue({ render: h => h(App) }).$mount('#app');
这套设计在一个页面只有一个 Vue 应用时没问题,但一旦同页面要跑两个应用(微前端、把 Vue 组件嵌进老项目、同页面多个独立挂载点),麻烦就来了:两个应用共享同一份全局配置,A 应用注册的全局组件 B 也能用,A 改了 Vue.config 会影响 B,测试用例之间的全局状态也会互相污染。
Vue3 的 createApp 把这些配置从全局搬到了应用实例上。每次调用 createApp 得到一个独立的实例,配置只对这个实例生效:
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.mount('#app');
两个应用互不影响
这是最直接的收益:
const app1 = createApp(AppOne);
app1.component('SharedBtn', RedButton);
app1.mount('#app-one');
const app2 = createApp(AppTwo);
app2.component('SharedBtn', BlueButton); // 同名不冲突
app2.mount('#app-two');
app1 里的 SharedBtn 是红色按钮,app2 里是蓝色,两者完全隔离。Vue2 里做不到这一点。
常用的实例方法
createApp 返回的 app 实例上的方法都返回实例本身,所以可以链式调用:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
createApp(App)
.use(createPinia())
.use(router)
.component('MyIcon', MyIcon)
.directive('focus', {
mounted(el) {
el.focus();
},
})
.provide('apiBase', '/admin-api')
.mount('#app');
各个方法的作用:
use(plugin, options):安装插件。同一个插件重复use只会生效一次,Vue 内部有去重component(name, comp):注册全局组件。只传 name 是取回已注册的组件directive(name, def):注册全局指令provide(key, value):应用级别的依赖注入,所有后代组件都能inject,不需要在根组件里写providemixin(mixin):注册全局 mixin,组合式 API 时代基本不该用了mount(selector | element):挂载,返回根组件实例(不是 app 实例,链式调用要注意这个断点)unmount():卸载应用,会触发所有组件的卸载钩子
挂载的行为细节
mount 的参数可以是选择器字符串也可以是真实 DOM 元素。它的行为和 Vue2 的 $mount 有个重要差异:Vue3 会把渲染结果追加进容器内部,Vue2 是替换掉容器元素本身。
<div id="app">这里的内容会被清空</div>
Vue3 挂载后,#app 这个 div 仍然存在,组件内容作为它的子节点。容器原有的 innerHTML 会被清掉——除了在 SSR 水合(hydration)模式下,那时原有内容是服务端渲染出来的,需要被复用。
另一个细节是 app.config 必须在 mount 之前配置好,挂载之后再改不生效:
const app = createApp(App);
// 全局错误处理,必须在 mount 前设置
app.config.errorHandler = (err, instance, info) => {
console.error('[vue error]', err, info);
// 上报到监控平台
};
// 全局属性,相当于 Vue2 的 Vue.prototype.$xxx
app.config.globalProperties.$formatDate = d => new Date(d).toLocaleString();
// 编译器选项,只在使用运行时编译版本时有效
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('my-');
app.mount('#app');
globalProperties 在模板里可以直接用 $formatDate(x),但在 <script setup> 里访问不到,得通过 getCurrentInstance() 取,比较别扭。所以更推荐用 provide/inject 或者干脆 import 一个工具函数,只有需要兼容老代码时才用 globalProperties。
createApp 内部做了什么
大致流程是这样的:
createApp 实际上是 runtime-dom 包里的一个函数,它先调用 ensureRenderer() 拿到渲染器。渲染器由 createRenderer(rendererOptions) 创建,rendererOptions 里是一组平台相关的 DOM 操作方法(createElement、insert、remove、patchProp 等)。这一层抽象是为了跨平台——换一套 options 就能渲染到 canvas 或者原生应用。
然后 createAppAPI(render) 返回真正的 createApp 函数,它创建 appContext(存放全局组件、指令、provides、config 的容器)和 app 实例对象。
app.mount(container) 被调用时:先用 createVNode(rootComponent, rootProps) 把根组件转成 vnode,把 appContext 挂到这个 vnode 上(这就是为什么全局注册的东西整棵树都能访问到),再调 render(vnode, container) 走进 patch 流程完成首次渲染,最后把 container 记录在 app._container 上供 unmount 使用。
有一点值得注意:app.mount 返回的是根组件的代理实例,不是 app 实例。所以下面这样写会拿到错的东西:
// 错误:app 是根组件实例,没有 unmount 方法
const app = createApp(App).mount('#app');
app.unmount(); // TypeError
// 正确:分开写
const app = createApp(App);
const vm = app.mount('#app');
app.unmount();
关于 unmount 和微前端
unmount() 会递归触发所有组件的 onUnmounted,清空容器,并把 app 标记为已卸载。这在微前端里是必需的——子应用切走时必须调用 unmount,否则定时器、事件监听、ResizeObserver 都会残留,多次进出同一个子应用会累积泄漏。
典型的微前端生命周期封装:
let app = null;
export async function mount(props) {
app = createApp(App);
app.use(router).use(createPinia());
app.mount(props.container?.querySelector('#app') ?? '#app');
}
export async function unmount() {
app?.unmount();
app = null; // 断开引用,让实例能被 GC
}
app = null 这一步别漏。unmount() 只是卸载了组件树,app 对象本身还持有 appContext,模块级变量一直引用着它的话整块内存回收不了。
另外同一个 app 实例卸载后不能再 mount,必须重新 createApp。

还没有留言,来发表第一条吧~