Vue2 与 Vue3 全面对比

中等 🟡Vue 生态
7 个标签
预计阅读时间:74 分钟
Vue2Vue3对比迁移指南Composition APIProxy响应式

Vue2 与 Vue3 全面对比

Vue3 于 2020 年 9 月正式发布(代号 One Piece),2022 年 2 月起成为默认版本,Vue2 已于 2023 年 12 月 31 日停止官方维护。相比 Vue2,Vue3 在性能、体积、API 设计、TypeScript 支持等方面带来了许多重大改进。本文从多个维度全面对比 Vue2 和 Vue3,帮助开发者理解两者的区别,并为迁移提供参考。

零、为什么要关心两者的区别

即使今天启动新项目一定会选 Vue3,理解两者区别依然至关重要,原因有三:

1.存量项目多:大量线上系统仍是 Vue2,维护、迁移都需要懂差异。
2.面试高频:Vue2/Vue3 对比是前端面试的经典必考题,尤其是响应式原理。
3.理解设计演进:从 Object.defineProperty 到 Proxy,从 Options 到 Composition,理解"为什么要变"比记住"变了什么"更有价值。

可以用一个类比:Vue2 像是一台功能齐全但零件焊死的一体机,Vue3 则是模块化的组装机——你只装需要的模块,性能更好、更容易扩展和维护。

一、性能对比

1. 打包体积优化

Vue2:

完整版本约 30KB(gzip)
所有功能都包含在核心包中
即使你不用某个特性,它的代码也会被打进包里

Vue3:

完整版本约 10KB(gzip)
模块化设计,支持 Tree Shaking
按需导入功能,用不到的 API 不会进入最终产物
javascriptCode
// Vue2:全部导入
import Vue from 'vue';

// Vue3:按需导入
import { ref, reactive, computed } from 'vue';

为什么 Vue3 能 Tree Shaking? 因为 Vue3 把全局 API 从 Vue 对象上的方法改成了独立导出的具名函数。打包工具(如 Rollup/Vite)能静态分析出哪些函数没被引用并删除。而 Vue2 里 Vue.nextTick、Vue.set 都挂在 Vue 对象上,无法被安全删除。

体积对比数据:

| 场景 | Vue2 | Vue3 | 变化 |

|------|------|------|------|

| 运行时核心(gzip) | 约 22KB | 约 10KB | -55% |

| 只用 ref/computed 的最小应用 | 约 22KB | 约 13.5KB | 显著更小 |

| Transition 等未使用特性 | 打包进去 | 被摇树删除 | 按需 |

2. 渲染性能

Vue2:

虚拟 DOM 基于 Snabbdom
全量 Diff 算法
静态节点每次都要比较

Vue3:

自研虚拟 DOM
编译时优化 + 运行时优化
静态节点提升(Static Hoisting)
补丁标志(Patch Flags)
事件处理函数缓存(Cache Handlers)
区块树(Block Tree)只追踪动态节点
javascriptCode
// Vue2 编译结果
function render() {
  return h('div', { id: 'app' }, [
    h('h1', {}, this.title),
    h('p', { class: 'static' }, 'Static text')
  ]);
}

// Vue3 编译结果(带优化)
const _hoisted_1 = h('p', { class: 'static' }, 'Static text');

function render() {
  return h('div', { id: 'app' }, [
    h('h1', {}, this.title),
    _hoisted_1
  ]);
}

补丁标志(Patch Flags)示例: Vue3 编译器会给动态节点打上标记,运行时只对比标记的部分:

javascriptCode
// 模板:<div :id="dynamicId" class="static">{{ text }}</div>
// 编译后(简化)
createElementVNode("div", {
  id: dynamicId,
  class: "static"
}, toDisplayString(text), 9 /* TEXT, PROPS */, ["id"])
// 数字 9 是 patchFlag,["id"] 表示只有 id 是动态 prop
// 更新时 class 完全跳过对比,只看 id 和文本

渲染性能对比(官方基准,10000 个组件):

| 指标 | Vue2 | Vue3 | 提升 |

|------|------|------|------|

| 初次渲染 | 基准 | 约快 1.3-2 倍 | 更快 |

| 更新渲染 | 基准 | 约快 1.3-2 倍 | 更快 |

| 内存占用 | 基准 | 约减少一半 | 更省 |

3. 响应式性能

Vue2(Object.defineProperty):

初始化时递归遍历所有属性,逐个 defineProperty
无法检测新增/删除属性(需 Vue.set / Vue.delete)
无法检测数组索引变化和 length 变化

Vue3(Proxy):

懒代理,访问时才递归处理嵌套对象
支持新增/删除属性检测
支持数组索引变化检测
支持 Map、Set、WeakMap、WeakSet 等数据结构

Vue2 响应式的经典局限演示:

javascriptCode
// Vue2 中这些操作不会触发视图更新
this.obj.newProp = 'value';        // 新增属性无效
delete this.obj.oldProp;           // 删除属性无效
this.arr[0] = 'newValue';          // 索引赋值无效
this.arr.length = 0;               // 修改 length 无效

// 必须使用特殊 API
this.$set(this.obj, 'newProp', 'value');
this.$delete(this.obj, 'oldProp');
this.arr.splice(0, 1, 'newValue');
javascriptCode
// Vue3 中一切照常工作
const obj = reactive({ a: 1 });
obj.newProp = 'value';   // 有效,视图更新
delete obj.a;            // 有效
const arr = reactive([1, 2, 3]);
arr[0] = 'newValue';     // 有效
arr.length = 0;          // 有效

为什么 Proxy 更强? Object.defineProperty 只能拦截"已存在的、被明确 define 过的属性"的读写;Proxy 是拦截"整个对象"的操作,包括属性新增、删除、in 判断等 13 种拦截操作,因此天然支持动态属性。

二、API 设计对比

1. Options API vs Composition API

Vue2 - Options API:

按选项(data、methods、computed、watch)组织代码。

vueCode
<script>
export default {
  data() {
    return {
      count: 0,
      user: null
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2;
    }
  },
  mounted() {
    this.fetchUser();
  }
};
</script>

Vue3 - Composition API:

按功能组织代码,相关逻辑集中在一起。

vueCode
<script setup>
import { ref, computed, onMounted } from 'vue';

const count = ref(0);
const user = ref(null);

const increment = () => {
  count.value++;
};

const doubleCount = computed(() => count.value * 2);

const fetchUser = async () => {
  // 获取用户
};

onMounted(() => {
  fetchUser();
});
</script>

核心差异的类比:Options API 像是把所有"工具按种类分抽屉"(螺丝刀一个抽屉、螺丝一个抽屉),修一个东西要开好几个抽屉;Composition API 像是把"修某个东西需要的所有工具装进一个工具箱",逻辑内聚。当组件变大时,Options API 的同一功能会散落在 data、methods、computed、watch 各处,而 Composition API 能把它们收拢在一起。

对比分析:

| 特性 | Options API | Composition API |

|------|-------------|-----------------|

| 代码组织 | 按选项分散 | 按功能集中 |

| 逻辑复用 | mixins | composables |

| TypeScript 支持 | 一般 | 优秀 |

| 学习曲线 | 低 | 较高 |

| 大组件可维护性 | 差(逻辑分散) | 好(逻辑内聚) |

| this 指向 | 依赖 this | 无 this,更直观 |

2. 逻辑复用:mixins vs composables

Vue2 mixins 的问题:

javascriptCode
// mixins/counter.js
export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() { this.count++; }
  }
};

// 使用
import counterMixin from './mixins/counter';
export default {
  mixins: [counterMixin]
  // 问题:count 和 increment 从哪来?不看 mixin 源码根本不知道
  // 多个 mixin 还可能命名冲突
};

Vue3 composables 的优势:

javascriptCode
// composables/useCounter.js
import { ref } from 'vue';

export function useCounter(initial = 0) {
  const count = ref(initial);
  const increment = () => count.value++;
  return { count, increment };
}

// 使用
import { useCounter } from './composables/useCounter';
// 来源清晰,可重命名,无隐式注入
const { count, increment } = useCounter(10);

mixins vs composables 对比:

| 问题 | mixins | composables |

|------|--------|-------------|

| 数据来源 | 不清晰(隐式注入) | 清晰(显式解构) |

| 命名冲突 | 容易发生 | 可自行重命名解决 |

| 多个复用 | 难以追踪 | 一目了然 |

| TS 类型推断 | 差 | 好 |

3. 生命周期对比

| Vue2 | Vue3 | 说明 |

|------|------|------|

| beforeCreate | setup() | 实例创建前 |

| created | setup() | 实例创建后 |

| beforeMount | onBeforeMount | 挂载前 |

| mounted | onMounted | 挂载后 |

| beforeUpdate | onBeforeUpdate | 更新前 |

| updated | onUpdated | 更新后 |

| beforeDestroy | onBeforeUnmount | 卸载前 |

| destroyed | onUnmounted | 卸载后 |

注意 Vue3 把 beforeDestroy/destroyed 更名为 onBeforeUnmount/onUnmounted,语义更准确(组件是"卸载"而非"销毁")。beforeCreate 和 created 被 setup() 取代,因为 setup 本身就运行在这两个时机之间。

javascriptCode
// Vue2 生命周期
export default {
  beforeCreate() {
    console.log('beforeCreate');
  },
  created() {
    console.log('created');
  },
  mounted() {
    console.log('mounted');
  },
  beforeDestroy() {
    console.log('beforeDestroy');
  }
};

// Vue3 生命周期
import {
  onBeforeMount,
  onMounted,
  onBeforeUnmount,
  onUnmounted
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => {
      console.log('onBeforeMount');
    });

    onMounted(() => {
      console.log('onMounted');
    });

    onBeforeUnmount(() => {
      console.log('onBeforeUnmount');
    });

    onUnmounted(() => {
      console.log('onUnmounted');
    });
  }
};

Vue3 新增的调试钩子:

javascriptCode
import { onRenderTracked, onRenderTriggered } from 'vue';

export default {
  setup() {
    // 追踪哪个响应式依赖被收集
    onRenderTracked((e) => {
      console.log('tracked:', e);
    });
    // 追踪哪个依赖变化触发了重渲染,排查性能问题利器
    onRenderTriggered((e) => {
      console.log('triggered:', e);
    });
  }
};

三、响应式 API 对比

1. 数据定义

Vue2:

javascriptCode
export default {
  data() {
    return {
      count: 0,
      user: {
        name: 'Alice',
        age: 25
      }
    };
  }
};

Vue3:

javascriptCode
import { ref, reactive } from 'vue';

// ref:适合基本类型
const count = ref(0);

// reactive:适合对象
const user = reactive({
  name: 'Alice',
  age: 25
});

ref vs reactive 该用哪个?

| 维度 | ref | reactive |

|------|-----|----------|

| 适用类型 | 任意(基本类型/对象) | 仅对象/数组/集合 |

| 访问方式 | .value(模板中自动解包) | 直接访问属性 |

| 解构 | 保持响应性 | 解构会丢失响应性 |

| 整体替换 | 可以(count.value = 新对象) | 不可直接替换整个对象 |

javascriptCode
// reactive 解构丢失响应性的坑
const state = reactive({ count: 0 });
let { count } = state;   // count 只是普通值,不再响应
count++;                  // state.count 不变

// 解决方案:toRefs
import { toRefs } from 'vue';
const { count } = toRefs(state); // count 是 ref,保持响应

实践建议:优先用 ref,它更统一(任何类型都能包),只在明确管理一组相关状态时用 reactive。

2. 计算属性

Vue2:

javascriptCode
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    };
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName;
    }
  }
};

Vue3:

javascriptCode
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed(() => {
  return firstName.value + ' ' + lastName.value;
});

可写计算属性(Vue3):

javascriptCode
const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value;
  },
  set(newValue) {
    [firstName.value, lastName.value] = newValue.split(' ');
  }
});
fullName.value = 'Jane Smith'; // 会拆分并回写

3. 监听器

Vue2:

javascriptCode
export default {
  data() {
    return {
      count: 0
    };
  },
  watch: {
    count(newVal, oldVal) {
      console.log('count changed:', newVal, oldVal);
    }
  }
};

Vue3:

javascriptCode
import { ref, watch, watchEffect } from 'vue';

const count = ref(0);

// 监听单个源
watch(count, (newVal, oldVal) => {
  console.log('count changed:', newVal, oldVal);
});

// watchEffect:自动追踪依赖
watchEffect(() => {
  console.log('count:', count.value);
});

watch 的更多用法:

javascriptCode
// 监听多个源
watch([firstName, lastName], ([newFirst, newLast]) => {
  console.log(newFirst, newLast);
});

// 监听 reactive 对象的某个属性(用 getter)
watch(() => user.age, (newAge) => {
  console.log('年龄变化', newAge);
});

// 深度监听 + 立即执行
watch(user, (val) => {
  console.log('user 深层变化', val);
}, { deep: true, immediate: true });

watch vs watchEffect 对比:

| 维度 | watch | watchEffect |

|------|-------|-------------|

| 依赖声明 | 显式指定 | 自动收集 |

| 首次执行 | 默认不执行 | 立即执行 |

| 拿到旧值 | 能 | 不能 |

| 适用场景 | 精确监听特定源 | 副作用随任意依赖自动运行 |

四、组件系统对比

1. 组件定义

Vue2:

javascriptCode
// 全局注册
Vue.component('my-component', {
  template: '<div>My Component</div>'
});

// 局部注册
export default {
  components: {
    MyComponent
  }
};

Vue3:

javascriptCode
import { defineComponent } from 'vue';
import MyComponent from './MyComponent.vue';

// defineComponent(可选,用于类型推断)
export default defineComponent({
  components: {
    MyComponent
  }
});

// <script setup> 自动注册
<script setup>
import MyComponent from './MyComponent.vue';
</script>

全局注册的差异:

javascriptCode
// Vue2:挂在全局 Vue 上,影响所有实例
Vue.component('MyComponent', MyComponent);

// Vue3:挂在具体 app 实例上,多个 app 互不影响
const app = createApp(App);
app.component('MyComponent', MyComponent);

Vue3 把全局配置从"全局单例 Vue"改为"每个 createApp 实例独立",解决了 Vue2 中"一处配置污染全局、多实例互相干扰"的问题。

2. Props 和 Emits

Vue2:

vueCode
<script>
export default {
  props: {
    title: String,
    count: {
      type: Number,
      default: 0
    }
  },
  methods: {
    handleClick() {
      this.$emit('click', { id: 1 });
    }
  }
};
</script>

Vue3:

vueCode
<script setup>
import { defineProps, defineEmits } from 'vue';

const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0
  }
});

const emit = defineEmits(['click', 'update']);

const handleClick = () => {
  emit('click', { id: 1 });
};
</script>

基于 TypeScript 的类型化 Props(Vue3 独有):

vueCode
<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
}

// 类型驱动的声明,配合 withDefaults 设默认值
const props = withDefaults(defineProps<Props>(), {
  count: 0
});

const emit = defineEmits<{
  (e: 'click', payload: { id: number }): void;
  (e: 'update', value: string): void;
}>();
</script>

这是 Vue3 相比 Vue2 在开发体验上的巨大飞跃:props 和 emits 都有完整的类型检查和自动补全。

3. Vue3 新增的内置组件

vueCode
<!-- Fragment:Vue3 支持多个根节点,Vue2 必须单根 -->
<template>
  <header>头部</header>
  <main>内容</main>
  <footer>底部</footer>
</template>
vueCode
<!-- Teleport:把内容渲染到 DOM 树的其他位置,如 body 下的弹窗 -->
<template>
  <Teleport to="body">
    <div class="modal">我被传送到 body 下了</div>
  </Teleport>
</template>
vueCode
<!-- Suspense:优雅处理异步组件的加载态 -->
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div>加载中...</div>
    </template>
  </Suspense>
</template>

五、状态管理对比

Vuex 3 vs Vuex 4 vs Pinia

Vuex 3(Vue2):

javascriptCode
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment({ commit }) {
      commit('increment');
    }
  }
});

Vuex 4(Vue3):

javascriptCode
import { createStore } from 'vuex';

export default createStore({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment({ commit }) {
      commit('increment');
    }
  }
});

Pinia(Vue3 推荐):

javascriptCode
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
});

// 组件中使用
const counterStore = useCounterStore();
counterStore.increment();

Pinia 的组合式写法(更贴近 Composition API):

javascriptCode
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }
  return { count, doubleCount, increment };
});

为什么 Pinia 取代 Vuex 成为官方推荐?

| 维度 | Vuex 4 | Pinia |

|------|--------|-------|

| mutations | 必须有,样板代码多 | 取消,直接改 state |

| TypeScript | 支持差,类型推断难 | 一流的类型支持 |

| 模块化 | 需 modules 嵌套 | 天然扁平,多 store |

| 体积 | 较大 | 约 1KB |

| DevTools | 支持 | 支持,且更好 |

| API 心智 | state/getters/mutations/actions 四段 | 去掉 mutations,更简洁 |

六、Vue Router 对比

Vue Router 3(Vue2):

javascriptCode
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const router = new VueRouter({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: Home
    }
  ]
});

Vue Router 4(Vue3):

javascriptCode
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: Home
    }
  ]
});

Router 3 与 4 的关键差异:

| 差异点 | Router 3 | Router 4 |

|--------|----------|----------|

| 实例创建 | new VueRouter() | createRouter() |

| 模式配置 | mode: 'history' | history: createWebHistory() |

| 组合式 API | 无 | useRoute() / useRouter() |

| 守卫 next | 必须调用 | 可用返回值代替 |

| 通配符 | path: '' | path: '/:pathMatch(.)*' |

组合式路由 API(Router 4 新增):

vueCode
<script setup>
import { useRoute, useRouter } from 'vue-router';

const route = useRoute();   // 相当于 this.$route
const router = useRouter();  // 相当于 this.$router

console.log(route.params.id);
router.push('/home');
</script>

七、迁移指南

从 Vue2 迁移到 Vue3

1. 更新依赖:

jsonCode
{
  "dependencies": {
    "vue": "^3.0.0",
    "vue-router": "^4.0.0",
    "vuex": "^4.0.0"
  }
}

2. 全局 API 变更:

javascriptCode
// Vue2
import Vue from 'vue';
Vue.use(Plugin);
Vue.component('MyComponent', MyComponent);

// Vue3
import { createApp } from 'vue';
const app = createApp(App);
app.use(Plugin);
app.component('MyComponent', MyComponent);
app.mount('#app');

3. 模板变更:

v-model 默认 prop 从 value 改为 modelValue
v-model 默认 event 从 input 改为 update:modelValue
v-if 优先级高于 v-for(Vue2 相反)
支持多个根节点(Fragment)
.sync 修饰符移除,用 v-model:propName 代替
vueCode
<!-- Vue2 .sync -->
<Child :title.sync="title" />
<!-- Vue3 等价写法 -->
<Child v-model:title="title" />

4. 移除的特性:

过滤器(filters)—— 改用计算属性或方法
全局 API:Vue.config.keyCodes
实例方法:$on, $off, $once(EventBus 模式需改用第三方库)
功能:inline-template
$children 属性
javascriptCode
// Vue2 过滤器
{{ price | currency }}
// Vue3 用计算属性或方法替代
{{ formatCurrency(price) }}

5. 迁移工具与策略:

使用官方 @vue/compat(迁移构建版本),可在 Vue2 兼容模式下逐步迁移
先跑 eslint-plugin-vue 找出不兼容用法
使用 gogocode 等自动化工具批量转换

迁移成本评估参考

| 项目规模 | 组件数 | 预估迁移工时 | 建议策略 |

|----------|--------|--------------|----------|

| 小型 | < 30 | 1-2 周 | 一次性重写 |

| 中型 | 30-150 | 1-2 月 | compat 渐进迁移 |

| 大型 | > 150 | 3-6 月 | 新功能上 Vue3,旧模块逐步迁 |

八、最佳实践建议

1. 新项目:

直接使用 Vue3 + Composition API +