Vue 实例挂载流程深度解析
概述
Vue 实例的挂载流程是理解 Vue 工作原理的核心。这个过程本质上是将我们编写的 Vue 组件(包含模板、数据和逻辑)转换成浏览器中真实 DOM 元素并显示出来的完整技术流程。
本文将详细解析从实例创建到最终 DOM 渲染的整个过程,并对比 Vue 2 和 Vue 3 的实现差异。
挂载流程概览
Vue 实例挂载可以分为三个核心阶段:
初始化阶段 → 模板编译阶段 → 渲染挂载阶段
↓ ↓ ↓
响应式数据 render函数 真实DOM每个阶段都有其特定的生命周期钩子和关键操作,让我们逐一深入分析。
第一阶段:初始化(Initialization)
创建实例与选项合并
Vue 2 方式
javascript
// Vue 2 实例创建
const vm = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
},
methods: {
handleClick() {
console.log(this.message);
}
}
});Vue 3 方式
javascript
// Vue 3 应用实例创建
import { createApp } from 'vue';
const app = createApp({
data() {
return {
message: 'Hello Vue 3!'
};
},
methods: {
handleClick() {
console.log(this.message);
}
}
});
app.mount('#app');关键差异:
- Vue 2:通过
new Vue()创建根实例,全局配置会影响所有实例 - Vue 3:通过
createApp()创建独立的应用实例,实现更好的隔离性
内部初始化流程
Vue 在实例创建过程中会依次执行以下初始化步骤:
javascript
// 简化的初始化流程(Vue 2)
function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
// 1. 合并选项
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
// 2. 初始化生命周期
initLifecycle(vm);
// 3. 初始化事件
initEvents(vm);
// 4. 初始化渲染
initRender(vm);
// 5. 调用 beforeCreate 钩子
callHook(vm, 'beforeCreate');
// 6. 初始化注入
initInjections(vm);
// 7. 初始化状态(核心)
initState(vm);
// 8. 初始化提供
initProvide(vm);
// 9. 调用 created 钩子
callHook(vm, 'created');
// 10. 如果有 el 选项,自动挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}响应式系统初始化(核心环节)
这是最关键的步骤,Vue 会将数据转换为响应式:
Vue 2 的响应式实现
javascript
// Vue 2 使用 Object.defineProperty
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 依赖收集
if (Dep.target) {
dep.depend();
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
// 通知更新
dep.notify();
}
});
}
// 初始化 data
function initData(vm) {
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {};
// 代理数据到实例上
const keys = Object.keys(data);
let i = keys.length;
while (i--) {
const key = keys[i];
proxy(vm, '_data', key);
}
// 观察数据
observe(data, true);
}Vue 3 的响应式实现
javascript
// Vue 3 使用 Proxy
function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
);
}
const mutableHandlers = {
get(target, key, receiver) {
// 依赖收集
track(target, 'get', key);
const result = Reflect.get(target, key, receiver);
// 嵌套对象也要变成响应式
if (isObject(result)) {
return reactive(result);
}
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 触发更新
trigger(target, 'set', key, value, oldValue);
}
return result;
}
};Vue 3 Proxy 的优势:
- 可以监听对象属性的新增和删除
- 可以监听数组的变化
- 性能更好,延迟创建响应式对象
- 支持 Map、Set 等更多数据类型
created 生命周期钩子
javascript
// 此时的实例状态
new Vue({
data: {
message: 'Hello'
},
created() {
// ✅ 可以访问数据
console.log(this.message); // "Hello"
// ✅ 可以调用方法
console.log(this.getMessage);
// ❌ $el 还不存在
console.log(this.$el); // undefined
// ❌ DOM 还没有渲染
console.log(document.getElementById('app')); // 可能存在但没有内容
}
});第二阶段:模板编译(Template Compilation)
模板查找优先级
Vue 按以下优先级查找模板:
javascript
// 1. 优先级最高:render 函数
new Vue({
render(h) {
return h('div', 'Hello from render function');
}
});
// 2. 次优先级:template 选项
new Vue({
template: '<div>{{ message }}</div>',
data: {
message: 'Hello from template'
}
});
// 3. 最低优先级:el 元素的 innerHTML
new Vue({
el: '#app' // 使用 <div id="app">的内容作为模板
});编译过程详解
模板编译包含三个关键步骤:
1. 解析(Parse)- 生成 AST
javascript
// 输入模板
const template = `
<div id="app">
<h1>{{ title }}</h1>
<p v-if="show">{{ message }}</p>
<button @click="handleClick">Click me</button>
</div>
`;
// 输出 AST(简化版)
const ast = {
type: 1, // 元素节点
tag: 'div',
attrsList: [{ name: 'id', value: 'app' }],
attrsMap: { id: 'app' },
children: [
{
type: 1,
tag: 'h1',
children: [
{
type: 2, // 表达式节点
expression: 'title',
text: '{{ title }}'
}
]
},
{
type: 1,
tag: 'p',
if: 'show', // v-if 指令
children: [
{
type: 2,
expression: 'message',
text: '{{ message }}'
}
]
}
// ... 更多子节点
]
};2. 优化(Optimize)- 标记静态节点
javascript
// 优化后的 AST 会标记静态节点
function optimize(ast) {
if (!ast) return;
// 标记静态节点
markStatic(ast);
// 标记静态根节点
markStaticRoots(ast);
}
function markStatic(node) {
node.static = isStatic(node);
if (node.type === 1) { // 元素节点
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
markStatic(child);
// 如果子节点不是静态的,父节点也不是静态的
if (!child.static) {
node.static = false;
}
}
}
}
// 判断是否为静态节点
function isStatic(node) {
if (node.type === 2) { // 表达式
return false;
}
if (node.type === 3) { // 纯文本
return true;
}
return !!(
!node.hasBindings && // 没有动态绑定
!node.if && !node.for && // 没有 v-if 或 v-for
!isBuiltInTag(node.tag) && // 不是内置标签
isPlatformReservedTag(node.tag) && // 是平台保留标签
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey)
);
}3. 生成(Generate)- 生成渲染函数
javascript
// 根据优化后的 AST 生成渲染函数代码
function generate(ast, options) {
const state = new CodegenState(options);
const code = ast ? genElement(ast, state) : '_c("div")';
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
};
}
// 生成的渲染函数代码示例
const renderCode = `
with(this) {
return _c('div', {
attrs: { "id": "app" }
}, [
_c('h1', [_v(_s(title))]),
(show) ? _c('p', [_v(_s(message))]) : _e(),
_c('button', {
on: { "click": handleClick }
}, [_v("Click me")])
])
}
`;
// 转换为可执行函数
const renderFunction = new Function(renderCode);渲染函数辅助方法说明:
_c:createElement,创建元素_v:createTextVNode,创建文本节点_s:toString,转换为字符串_e:createEmptyVNode,创建空节点
编译时 vs 运行时
javascript
// 完整版 Vue(包含编译器)
// 可以在浏览器中编译模板
new Vue({
template: '<div>{{ message }}</div>',
data: { message: 'Hello' }
});
// 运行时版 Vue(不包含编译器)
// 需要预编译的渲染函数
new Vue({
render(h) {
return h('div', this.message);
},
data: { message: 'Hello' }
});第三阶段:渲染挂载(Render & Mount)
beforeMount 生命周期
javascript
new Vue({
el: '#app',
template: '<div>{{ message }}</div>',
data: {
message: 'Hello'
},
beforeMount() {
// ✅ 模板已编译,render 函数已准备好
console.log(typeof this.$options.render); // "function"
// ❌ DOM 还未生成
console.log(this.$el); // undefined
// 可以在这里修改数据,不会触发额外的渲染
this.message = 'Modified in beforeMount';
}
});执行渲染函数生成 VNode
javascript
// 渲染函数执行过程
function mountComponent(vm, el) {
vm.$el = el;
// 渲染 Watcher
const updateComponent = () => {
// 执行渲染函数生成 VNode
const vnode = vm._render();
// 将 VNode 渲染成真实 DOM
vm._update(vnode);
};
new Watcher(vm, updateComponent, noop, {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true);
}
// VNode 示例
const vnode = {
tag: 'div',
data: {
attrs: { id: 'app' }
},
children: [
{
tag: 'h1',
children: [
{
text: 'Hello Vue!',
isText: true
}
]
}
],
context: vm,
elm: null // 对应的真实 DOM 节点
};DOM 渲染过程
javascript
// 简化的渲染过程
function patch(oldVnode, vnode) {
if (isUndef(oldVnode)) {
// 初始化渲染
createElm(vnode);
} else {
// 更新渲染(diff 算法)
patchVnode(oldVnode, vnode);
}
}
function createElm(vnode, insertedVnodeQueue, parentElm, refElm) {
const { tag, data, children, text, context } = vnode;
if (isDef(tag)) {
// 创建元素节点
vnode.elm = document.createElement(tag);
// 设置属性
if (isDef(data)) {
updateAttrs(vnode.elm, {}, data.attrs || {});
updateClass(vnode.elm, {}, data.class);
updateStyle(vnode.elm, {}, data.style);
// ... 其他属性更新
}
// 创建子节点
createChildren(vnode, children, insertedVnodeQueue);
// 插入到父节点
insert(parentElm, vnode.elm, refElm);
} else if (isTrue(vnode.isComment)) {
// 创建注释节点
vnode.elm = document.createComment(text);
insert(parentElm, vnode.elm, refElm);
} else {
// 创建文本节点
vnode.elm = document.createTextNode(text);
insert(parentElm, vnode.elm, refElm);
}
}
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null);
}
} else if (isPrimitive(vnode.text)) {
vnode.elm.appendChild(document.createTextNode(String(vnode.text)));
}
}mounted 生命周期
javascript
new Vue({
el: '#app',
template: '<div ref="container">{{ message }}</div>',
data: {
message: 'Hello'
},
mounted() {
// ✅ DOM 已经渲染完成
console.log(this.$el); // <div>Hello</div>
// ✅ 可以访问 DOM 元素
console.log(this.$refs.container); // <div>Hello</div>
// ✅ 可以进行 DOM 操作
this.$el.style.color = 'red';
// ✅ 适合进行网络请求
this.fetchData();
}
});完整流程时序图
mermaid
sequenceDiagram
participant User as 用户代码
participant Vue as Vue实例
participant Compiler as 编译器
participant Renderer as 渲染器
participant DOM as DOM
User->>Vue: new Vue(options) / createApp
Vue->>Vue: 合并选项
Vue->>Vue: 初始化生命周期、事件
Vue->>Vue: initState (响应式)
Vue->>Vue: callHook('created')
Vue->>Compiler: 查找模板
Compiler->>Compiler: Parse (生成 AST)
Compiler->>Compiler: Optimize (标记静态)
Compiler->>Compiler: Generate (生成 render)
Compiler->>Vue: 返回 render 函数
Vue->>Vue: callHook('beforeMount')
Vue->>Renderer: 执行 render 函数
Renderer->>Renderer: 生成 VNode 树
Renderer->>DOM: 创建真实 DOM
DOM->>Vue: 挂载完成
Vue->>Vue: callHook('mounted')Vue 2 vs Vue 3 关键差异
实例创建方式
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 创建方式 | new Vue() | createApp() |
| 全局 API | 挂载在 Vue 构造函数上 | 挂载在应用实例上 |
| 多实例隔离 | 共享全局配置 | 每个应用独立配置 |
javascript
// Vue 2 - 全局配置影响所有实例
Vue.config.productionTip = false;
Vue.use(VueRouter);
const app1 = new Vue({ /* ... */ });
const app2 = new Vue({ /* ... */ }); // 也会受到全局配置影响
// Vue 3 - 独立的应用实例
const app1 = createApp(App1);
app1.config.globalProperties.$http = axios;
app1.use(Router1);
const app2 = createApp(App2); // 不会受到 app1 配置影响
app2.use(Router2);响应式系统
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 实现方式 | Object.defineProperty | Proxy |
| 监听能力 | 属性读写 | 属性增删、读写 |
| 性能 | 递归遍历所有属性 | 懒创建,按需代理 |
| 数据类型支持 | Object、Array | Object、Array、Map、Set等 |
组合式 API
javascript
// Vue 2 - 选项式 API
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
},
created() {
console.log('Component created');
}
};
// Vue 3 - 组合式 API
import { ref, onMounted } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
onMounted(() => {
console.log('Component mounted');
});
return {
count,
increment
};
}
};性能优化要点
编译时优化
- 静态提升(Static Hoisting)
javascript
// 编译前
<div>
<p>Static text</p>
<p>{{ dynamic }}</p>
</div>
// 编译后(Vue 3)
const _hoisted_1 = createVNode("p", null, "Static text");
function render() {
return createVNode("div", null, [
_hoisted_1, // 静态节点被提升,不会重复创建
createVNode("p", null, dynamic.value)
]);
}- 预字符串化(Pre-stringify)
javascript
// 大量静态内容会被预字符串化
const _hoisted_1 = createStaticVNode(
"<div><p>Static 1</p><p>Static 2</p><p>Static 3</p></div>",
3
);运行时优化
- 事件监听缓存
javascript
// Vue 3 会缓存事件处理函数
<button @click="handler">Click</button>
// 编译后
createVNode("button", {
onClick: cache[0] || (cache[0] = (...args) => handler(...args))
}, "Click")- Block Tree 优化
javascript
// Vue 3 的 Block Tree 只会 diff 动态节点
<div> <!-- Block -->
<p>static</p>
<p>{{ dynamic1 }}</p> <!-- 动态节点 -->
<span>
<p>{{ dynamic2 }}</p> <!-- 动态节点 -->
</span>
</div>常见问题与注意事项
1. 异步组件的挂载
javascript
// 异步组件的生命周期
Vue.component('async-component', () =>
import('./AsyncComponent.vue').then(module => {
// 这里的 mounted 会在异步加载完成后触发
return module.default;
})
);2. 服务端渲染的差异
javascript
// 服务端渲染时的生命周期
export default {
beforeCreate() {
// 服务端 + 客户端都会执行
},
created() {
// 服务端 + 客户端都会执行
},
beforeMount() {
// 仅在客户端执行
},
mounted() {
// 仅在客户端执行
}
};3. Keep-alive 组件的特殊情况
javascript
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
// Keep-alive 组件有特殊的生命周期
export default {
activated() {
// 被 keep-alive 缓存的组件激活时调用
},
deactivated() {
// 被 keep-alive 缓存的组件停用时调用
}
};总结
Vue 实例的挂载流程是一个精心设计的过程,它将声明式的模板和数据转换为命令式的 DOM 操作。理解这个流程有助于:
- 性能优化:了解哪些操作发生在编译时,哪些在运行时
- 问题调试:知道在生命周期的哪个阶段可以执行特定操作
- 深入理解:掌握响应式系统和虚拟 DOM 的工作原理
- 最佳实践:合理使用生命周期钩子和优化渲染性能
从 Vue 2 到 Vue 3 的演进,不仅在语法上提供了更多选择,更在底层实现上带来了显著的性能提升和更好的 TypeScript 支持。掌握这些原理,能够帮助我们写出更高效、更可维护的 Vue 应用。