前言
Vue.js 作为一款流行的前端框架,经历了从 Vue2 到 Vue3 的重大升级。Vue3 不仅仅是一个版本更新,更是一次全面的重构,带来了性能、开发体验和功能上的巨大提升。本文将从多个维度深入对比 Vue2 和 Vue3,帮助开发者更好地理解两者之间的差异和 Vue3 的优势。
1. 响应式原理对比
Vue2 的响应式系统
Vue2 使用 Object.defineProperty
来实现响应式数据绑定。这个 API 可以劫持对象属性的 getter 和 setter,从而在数据变化时通知视图更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function defineReactive(obj, key, val) { observe(val);
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log(`获取属性 ${key}: ${val}`); return val; }, set(newVal) { if (newVal === val) return; console.log(`设置属性 ${key}: ${newVal}`); val = newVal; notify(); }, }); }
function observe(obj) { if (typeof obj !== "object" || obj === null) { return; }
Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); }
|
Vue2 响应式系统的局限性
无法检测数组索引的变化:
1 2 3 4 5 6 7 8 9 10 11
| var vm = new Vue({ data: { items: ["a", "b", "c"], }, });
vm.items[1] = "x";
vm.items.length = 0;
|
无法检测对象属性的添加或删除:
1 2 3 4 5 6 7 8 9 10 11
| var vm = new Vue({ data: { a: 1, }, });
vm.b = 2;
delete vm.a;
|
为了解决这些问题,Vue2 重写了数组的一些方法(push、pop、shift、unshift、splice、sort、reverse),并提供了 Vue.set
(或 vm.$set
) 和 Vue.delete
(或 vm.$delete
) 方法。
Vue3 的响应式系统
Vue3 完全重写了响应式系统,采用 ES2015 的 Proxy
来替代 Object.defineProperty
。Proxy 可以拦截对象的更多操作,提供了更强大的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function reactive(target) { if (typeof target !== "object" || target === null) { return target; }
const handler = { get(target, key, receiver) { console.log(`获取属性 ${key.toString()}`); const result = Reflect.get(target, key, receiver); return typeof result === "object" ? reactive(result) : result; }, set(target, key, value, receiver) { console.log(`设置属性 ${key.toString()}: ${value}`); const result = Reflect.set(target, key, value, receiver); trigger(target, key); return result; }, deleteProperty(target, key) { console.log(`删除属性 ${key.toString()}`); const result = Reflect.deleteProperty(target, key); trigger(target, key); return result; }, };
return new Proxy(target, handler); }
|
Vue3 响应式系统的改进
可以检测数组索引的变化:
1 2 3 4 5 6 7 8 9 10 11
| import { reactive } from "vue";
const state = reactive({ items: ["a", "b", "c"], });
state.items[1] = "x";
state.items.length = 0;
|
可以检测对象属性的添加或删除:
1 2 3 4 5 6 7 8 9 10 11
| import { reactive } from "vue";
const state = reactive({ a: 1, });
state.b = 2;
delete state.a;
|
响应式系统优化总结
特性 |
Vue2 |
Vue3 |
优化点 |
实现方式 |
Object.defineProperty |
Proxy |
Proxy 功能更强大,能拦截更多操作 |
数组索引检测 |
不支持 |
支持 |
解决了 Vue2 的数组变化检测问题 |
对象属性增删 |
不支持 |
支持 |
无需使用 $set/$delete |
性能 |
递归遍历所有属性 |
懒劫持 |
初始化更快,内存占用更少 |
TypeScript 支持 |
有限 |
完善 |
更好的类型推导 |
2. 虚拟 DOM 和 Diff 算法对比
Vue2 的虚拟 DOM 和 Diff 算法
Vue2 使用基于 Snabbdom 的虚拟 DOM 实现,采用双端 Diff 算法。该算法通过同时从新旧子节点的两端开始比较,尽可能减少 DOM 操作。
Vue2 Diff 算法的特点:
- 双端比较:同时从新旧子节点的两端开始比较
- 四种比较方式:
- 新前与旧前
- 新后与旧后
- 新后与旧前(需要移动节点)
- 新前与旧后(需要移动节点)
- 时间复杂度:O(n)
Vue3 的虚拟 DOM 和 Diff 算法
Vue3 对虚拟 DOM 进行了重写,主要优化包括:
静态提升(Static Hoisting)
Vue3 会自动提升模板中的静态节点,避免在重新渲染时重新创建:
1 2 3 4 5 6 7 8 9 10 11
| function render() { return h("div", [h("p", "静态文本"), h("p", this.dynamicValue)]); }
const _hoisted_1 = h("p", "静态文本");
function render() { return h("div", [_hoisted_1, h("p", this.dynamicValue)]); }
|
预字符串化(Pre-stringification)
对于连续的静态节点,Vue3 会预编译成字符串,进一步提升性能:
1 2 3 4 5
| const _hoisted_1 = _createStaticVNode( "<p>静态文本1</p><p>静态文本2</p><p>静态文本3</p>", 3 );
|
Block Tree 优化
Vue3 引入了 Block Tree 概念,只对动态节点进行追踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function render(_ctx, _cache) { return ( _openBlock(), _createElementBlock("div", null, [ _hoisted_1, _createElementVNode( "p", null, _toDisplayString(_ctx.dynamicValue), 1 ), ]) ); }
|
Patch Flag 优化
Vue3 为动态节点添加了 Patch Flag,标记节点的动态类型:
1 2 3 4 5 6 7
| createElementVNode( "p", null, _toDisplayString(_ctx.dynamicValue), 1 );
|
虚拟 DOM 和 Diff 算法优化总结
特性 |
Vue2 |
Vue3 |
优化点 |
静态节点处理 |
每次重新创建 |
静态提升 |
减少内存占用和创建开销 |
连续静态节点 |
逐个创建 |
预字符串化 |
大幅提升渲染性能 |
动态节点追踪 |
全量比较 |
Block Tree |
只比较动态节点,减少比较次数 |
更新标记 |
无 |
Patch Flag |
精确更新,避免不必要的比较 |
3. 生命周期对比
Vue2 生命周期
Vue2 的生命周期可以分为四个阶段:
创建阶段:
beforeCreate
:实例创建前调用
created
:实例创建后调用
挂载阶段:
beforeMount
:挂载开始前调用
mounted
:挂载完成后调用
更新阶段:
beforeUpdate
:数据更新前调用
updated
:数据更新后调用
销毁阶段:
beforeDestroy
:实例销毁前调用
destroyed
:实例销毁后调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export default { beforeCreate() { console.log("beforeCreate"); }, created() { console.log("created"); }, beforeMount() { console.log("beforeMount"); }, mounted() { console.log("mounted"); }, beforeUpdate() { console.log("beforeUpdate"); }, updated() { console.log("updated"); }, beforeDestroy() { console.log("beforeDestroy"); }, destroyed() { console.log("destroyed"); }, };
|
Vue3 生命周期
Vue3 在保持与 Vue2 相似生命周期的同时,也引入了 Composition API,带来了新的生命周期钩子:
选项式 API 生命周期(与 Vue2 类似)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export default { beforeCreate() { console.log("beforeCreate"); }, created() { console.log("created"); }, beforeMount() { console.log("beforeMount"); }, mounted() { console.log("mounted"); }, beforeUpdate() { console.log("beforeUpdate"); }, updated() { console.log("updated"); }, beforeUnmount() { console.log("beforeUnmount"); }, unmounted() { console.log("unmounted"); }, };
|
组合式 API 生命周期
在 Vue3 的 Composition API 中,生命周期钩子有了新的命名方式,它们都带有 on
前缀:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, } from "vue";
export default { setup() {
onBeforeMount(() => { console.log("onBeforeMount"); });
onMounted(() => { console.log("onMounted"); });
onBeforeUpdate(() => { console.log("onBeforeUpdate"); });
onUpdated(() => { console.log("onUpdated"); });
onBeforeUnmount(() => { console.log("onBeforeUnmount"); });
onUnmounted(() => { console.log("onUnmounted"); });
return {}; }, };
|
生命周期优化总结
Vue2 选项式 API |
Vue3 选项式 API |
Vue3 组合式 API |
优化点 |
beforeCreate |
beforeCreate |
setup() |
setup 替代了 beforeCreate 和 created |
created |
created |
setup() |
统一在 setup 中处理初始化逻辑 |
beforeMount |
beforeMount |
onBeforeMount |
命名更统一 |
mounted |
mounted |
onMounted |
命名更统一 |
beforeUpdate |
beforeUpdate |
onBeforeUpdate |
命名更统一 |
updated |
updated |
onUpdated |
命名更统一 |
beforeDestroy |
beforeUnmount |
onBeforeUnmount |
术语更准确 |
destroyed |
unmounted |
onUnmounted |
术语更准确 |
4. Options API 与 Composition API 对比
Options API(选项式 API)
Options API 是 Vue2 和 Vue3 都支持的传统 API 风格,将组件的逻辑按选项(data、methods、computed 等)分类组织。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| export default { data() { return { count: 0, userList: [], }; }, computed: { doubleCount() { return this.count * 2; }, }, methods: { increment() { this.count++; }, async fetchUsers() { this.userList = await api.getUsers(); }, }, watch: { count(newVal, oldVal) { console.log(`count changed from ${oldVal} to ${newVal}`); }, }, mounted() { this.fetchUsers(); }, };
|
Composition API(组合式 API)
Composition API 是 Vue3 引入的新特性,允许开发者基于逻辑关注点组织代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import { ref, computed, watch, onMounted } from "vue";
export default { setup() { const count = ref(0); const userList = ref([]);
const doubleCount = computed(() => count.value * 2);
const increment = () => { count.value++; };
const fetchUsers = async () => { userList.value = await api.getUsers(); };
watch(count, (newVal, oldVal) => { console.log(`count changed from ${oldVal} to ${newVal}`); });
onMounted(() => { fetchUsers(); });
return { count, userList, doubleCount, increment, }; }, };
|
<script setup>
语法糖
Vue3.2 引入了 <script setup>
语法糖,进一步简化了 Composition API 的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <script setup> import { ref, computed, watch, onMounted } from "vue";
// 数据 const count = ref(0); const userList = ref([]);
// 计算属性 const doubleCount = computed(() => count.value * 2);
// 方法 const increment = () => { count.value++; };
const fetchUsers = async () => { // 获取用户列表 userList.value = await api.getUsers(); };
// 监听器 watch(count, (newVal, oldVal) => { console.log(`count changed from ${oldVal} to ${newVal}`); });
// 生命周期 onMounted(() => { fetchUsers(); }); </script>
<template> <div> <p>Count: {{ count }}</p> <p>Double Count: {{ doubleCount }}</p> <button @click="increment">Increment</button> <ul> <li v-for="user in userList" :key="user.id"> {{ user.name }} </li> </ul> </div> </template>
|
Options API 与 Composition API 对比总结
特性 |
Options API |
Composition API |
优化点 |
代码组织方式 |
按选项分类 |
按逻辑功能组织 |
更好地组织复杂组件逻辑 |
逻辑复用 |
Mixins |
Composable 函数 |
避免 mixins 的命名冲突问题 |
TypeScript 支持 |
有限 |
完善 |
更好的类型推导 |
学习曲线 |
简单 |
需要适应 |
对新手更友好,对复杂应用更有优势 |
代码压缩 |
一般 |
更好 |
更小的打包体积 |
5. 思考:Composition API 封装与 Vue2 中的模块封装
问题提出
在 Vue2 中,我们可以通过封装具体的类、方法和模块来实现逻辑复用,为什么还需要 Composition API 呢?
Vue2 中的模块封装示例
在 Vue2 中,我们确实可以通过封装模块来实现逻辑复用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| export class UserModule { constructor(vm) { this.vm = vm; }
async fetchUsers() { this.vm.loading = true; try { this.vm.userList = await api.getUsers(); } finally { this.vm.loading = false; } }
addUser(user) { this.vm.userList.push(user); } }
export default { data() { return { userList: [], loading: false, }; }, created() { this.userModule = new UserModule(this); }, methods: { async fetchUsers() { await this.userModule.fetchUsers(); }, }, };
|
Composition API 的优势
尽管 Vue2 中可以使用类和模块封装,但 Composition API 仍有以下优势:
1. 响应式集成
Composition API 与 Vue 的响应式系统深度集成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| export function useUser() { const userList = ref([]); const loading = ref(false);
const fetchUsers = async () => { loading.value = true; try { userList.value = await api.getUsers(); } finally { loading.value = false; } };
return { userList, loading, fetchUsers, }; }
export default { setup() { const { userList, loading, fetchUsers } = useUser();
onMounted(() => { fetchUsers(); });
return { userList, loading, fetchUsers, }; }, };
|
2. 更好的类型推导
Composition API 提供了更好的 TypeScript 支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| export function useUser() { const userList = ref<User[]>([]); const loading = ref<boolean>(false);
const fetchUsers = async () => { loading.value = true; try { userList.value = await api.getUsers(); } finally { loading.value = false; } };
return { userList, loading, fetchUsers, }; }
|
3. 更自然的逻辑组合
Composition API 允许更自然地组合不同功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| export default { setup() { const { userList, loading: userLoading, fetchUsers } = useUser();
const { page, pageSize, total, setPage } = usePagination();
const { searchKeyword, search } = useSearch();
const fetchUsersWithPagination = async () => { await fetchUsers({ page: page.value, pageSize: pageSize.value, keyword: searchKeyword.value, }); };
return { userList, userLoading, page, pageSize, total, searchKeyword, fetchUsersWithPagination, setPage, search, }; }, };
|
4. 与模板的无缝集成
Composition API 返回的数据可以直接在模板中使用,无需额外处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script setup> const { userList, loading, fetchUsers } = useUser(); </script>
<template> <div> <div v-if="loading">Loading...</div> <ul v-else> <li v-for="user in userList" :key="user.id"> {{ user.name }} </li> </ul> <button @click="fetchUsers">Refresh</button> </div> </template>
|
类似对比示例:状态管理
Vue2 中的状态管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| class Store { constructor() { this.state = { count: 0, }; }
increment() { this.state.count++; }
decrement() { this.state.count--; } }
export default { data() { return { localCount: this.$store.state.count, }; }, watch: { "$store.state.count"(newVal) { this.localCount = newVal; }, }, methods: { increment() { this.$store.increment(); }, }, };
|
Vue3 Composition API 状态管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import { ref } from 'vue'
const count = ref(0)
export function useCounter() { const increment = () => { count.value++ }
const decrement = () => { count.value-- }
return { count, increment, decrement } }
<script setup> import { useCounter } from './useCounter'
const { count, increment, decrement } = useCounter() </script>
<template> <div> <p>Count: {{ count }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> </div> </template>
|
封装方式对比总结
特性 |
Vue2 模块封装 |
Composition API |
优化点 |
响应式集成 |
需要手动处理 |
原生支持 |
简化响应式数据处理 |
代码组织 |
按功能划分文件 |
按逻辑关注点组织 |
更符合逻辑思维 |
复用性 |
需要实例化 |
直接导入使用 |
更简洁的复用方式 |
类型支持 |
有限 |
完善 |
更好的开发体验 |
与 Vue 集成 |
松耦合 |
紧耦合 |
更自然的 Vue 开发体验 |
总结
通过对 Vue2 和 Vue3 在多个维度的对比,我们可以看到 Vue3 在各个方面都有显著的改进:
- 响应式系统:从 Object.defineProperty 升级到 Proxy,解决了 Vue2 的诸多限制
- 虚拟 DOM 和 Diff 算法:引入静态提升、预字符串化、Block Tree 等优化,大幅提升渲染性能
- 生命周期:保持兼容性的同时,通过 Composition API 提供了更灵活的组织方式
- API 设计:Composition API 提供了更好的逻辑组织和复用能力,解决了 Options API 在大型项目中的局限性
虽然 Vue2 中也可以通过模块封装实现逻辑复用,但 Composition API 与 Vue 的响应式系统深度集成,提供了更好的开发体验和更强的功能。对于新项目,建议使用 Vue3 和 Composition API;对于已有 Vue2 项目,可以根据实际情况考虑是否升级。
如果你想了解更多 Vue 相关的知识,可以查看我们之前的文章,比如Vue 路由传参的几种方式或者为什么 vue template 中不用加.value?。