Skip to content

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 2Vue 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 2Vue 3
实现方式Object.definePropertyProxy
监听能力属性读写属性增删、读写
性能递归遍历所有属性懒创建,按需代理
数据类型支持Object、ArrayObject、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
    };
  }
};

性能优化要点

编译时优化

  1. 静态提升(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)
  ]);
}
  1. 预字符串化(Pre-stringify)
javascript
// 大量静态内容会被预字符串化
const _hoisted_1 = createStaticVNode(
  "<div><p>Static 1</p><p>Static 2</p><p>Static 3</p></div>", 
  3
);

运行时优化

  1. 事件监听缓存
javascript
// Vue 3 会缓存事件处理函数
<button @click="handler">Click</button>

// 编译后
createVNode("button", {
  onClick: cache[0] || (cache[0] = (...args) => handler(...args))
}, "Click")
  1. 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 操作。理解这个流程有助于:

  1. 性能优化:了解哪些操作发生在编译时,哪些在运行时
  2. 问题调试:知道在生命周期的哪个阶段可以执行特定操作
  3. 深入理解:掌握响应式系统和虚拟 DOM 的工作原理
  4. 最佳实践:合理使用生命周期钩子和优化渲染性能

从 Vue 2 到 Vue 3 的演进,不仅在语法上提供了更多选择,更在底层实现上带来了显著的性能提升和更好的 TypeScript 支持。掌握这些原理,能够帮助我们写出更高效、更可维护的 Vue 应用。

基于 VitePress 构建