Vue2与Vue3响应式机制对比及解决方案
Vue2使用Object.defineProperty实现响应式,存在多个响应式丢失场景:
- 直接添加/删除属性需使用Vue.set/Vue.delete
- 数组索引修改需使用Vue.set或splice
- 解构赋值会丢失响应式
Vue3基于Proxy的响应式系统更完善:
- 自动响应属性添加/删除
- 直接数组操作保持响应
- 仍需toRefs保持解构响应式
状态管理方案对比:
- Vuex3/4需通过mutations修改状态
- Pinia支持直接修改,自动响应式
- 解构状态需使用storeToRefs
最佳实践:
- Vue2使用Vue.set处理动态属性
- Vue3优先使用reactive+toRefs
- Pinia直接修改状态,配合$patch批量更新
Vue 2 Vue 3 响应式丢失场景及解决方案
| 响应式丢失场景 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) | 解决方案 |
|---|---|---|---|
| 1. 直接添加新属性 | ❌ 丢失响应式 | ✅ 自动响应 | Vue 2: Vue.set(obj, key, value) Vue 3: 直接赋值 obj.newKey = value |
| 2. 直接删除属性 | ❌ 丢失响应式 | ✅ 自动响应 | Vue 2: Vue.delete(obj, key) Vue 3: delete obj.key |
| 3. 通过索引修改数组 | ❌ 丢失响应式 | ✅ 自动响应 | Vue 2: Vue.set(arr, index, value) 或 arr.splice() Vue 3: 直接赋值 arr[index] = value |
| 4. 修改数组长度 | ❌ 丢失响应式 | ✅ 自动响应 | Vue 2: arr.splice(newLength) Vue 3: arr.length = newLength |
| 5. 解构赋值 | ❌ 丢失响应式 | ❌ 丢失响应式 | 使用 toRefs 或 toRef 保持响应式 |
| 6. 直接导出响应式对象 | ⚠️ 部分丢失 | ⚠️ 部分丢失 | 使用 reactive + toRefs 或 computed |
| 7. 将响应式对象赋值给新变量 | ❌ 丢失响应式 | ❌ 丢失响应式 | 保持引用关系,或使用 toRefs/toRef |
| 8. 函数参数传递 | ❌ 丢失响应式 | ❌ 丢失响应式 | 传递 ref 对象而非值,或使用 computed |
| 9. 响应式对象中的嵌套对象 | ⚠️ 需递归转换 | ✅ 懒代理(自动响应) | Vue 2: 预先定义或 Vue.set Vue 3: 自动处理 |
| 10. Map/Set 等集合类型 | ❌ 不支持 | ✅ 支持 | Vue 3 可直接使用 reactive(new Map()) |
| 11. 类实例属性 | ❌ 不支持 | ✅ 支持 | Vue 3 可直接代理类实例 |
| 12. 异步操作中赋值 | ⚠️ 需注意 this 指向 | ⚠️ 需注意 this 指向 | 使用箭头函数或保存 this 引用 |
详细说明与代码示例
1. 直接添加新属性
javascript
// Vue 2
data() {
return { user: { name: 'Alice' } }
},
methods: {
addAge() {
this.user.age = 25 // ❌ 非响应式
this.$set(this.user, 'age', 25) // ✅ 响应式
}
}
// Vue 3
const state = reactive({ user: { name: 'Alice' } })
state.user.age = 25 // ✅ 自动响应式
2. 直接删除属性
javascript
// Vue 2
delete this.user.age // ❌ 非响应式
this.$delete(this.user, 'age') // ✅ 响应式
// Vue 3
delete state.user.age // ✅ 自动响应式
3-4. 数组操作
javascript
// Vue 2
this.items[0] = newValue // ❌ 非响应式
this.items.length = 0 // ❌ 非响应式
this.$set(this.items, 0, newValue) // ✅
this.items.splice(0, 1, newValue) // ✅
this.items.splice(0, this.items.length) // ✅
// Vue 3
const items = reactive([1, 2, 3])
items[0] = 100 // ✅ 自动响应式
items.length = 0 // ✅ 自动响应式
5. 解构赋值(两者都丢失)
javascript
// ❌ 错误示例
const state = reactive({ count: 0, name: 'Alice' })
let { count, name } = state // 失去响应式
count++ // 不会触发更新
// ✅ 解决方案
import { toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Alice' })
const { count, name } = toRefs(state) // 保持响应式
count.value++ // 触发更新
// 单个属性使用 toRef
const count = toRef(state, 'count')
count.value++ // 触发更新
6. 直接导出响应式对象
javascript
// Vue 2 (composables)
export default function useCounter() {
let count = ref(0) // Vue 2.7+ 支持
const increment = () => count.value++
return { count, increment } // ✅ 保持响应式
}
// Vue 3
export default function useCounter() {
const state = reactive({ count: 0 })
const increment = () => state.count++
return { ...toRefs(state), increment } // ✅ 使用 toRefs 保持响应式
}
7. 将响应式对象赋值给新变量
javascript
// ❌ 错误示例
const state = reactive({ count: 0 })
let newState = state // newState 和 state 引用同一个对象
newState.count = 1 // ✅ 仍然响应式
let { count } = state // ❌ 失去响应式
// ✅ 正确做法
const count = toRef(state, 'count') // 保持响应式引用
8. 函数参数传递
javascript
// ❌ 错误示例
const state = reactive({ count: 0 })
function updateCount(count) {
count = 10 // 失去响应式
}
updateCount(state.count)
// ✅ 解决方案
function updateCount(countRef) {
countRef.value = 10 // 传递 ref 对象
}
updateCount(toRef(state, 'count'))
// 或使用 computed
const doubleCount = computed(() => state.count * 2)
9. 嵌套对象响应式
javascript
// Vue 2
data() {
return {
user: {
profile: {
name: 'Alice' // 需要预先定义才能响应式
}
}
}
},
methods: {
addNested() {
// ❌ 动态添加深层属性可能丢失响应式
this.user.profile.age = 25
this.$set(this.user.profile, 'age', 25) // ✅ 需要 $set
}
}
// Vue 3
const state = reactive({
user: {
profile: {
name: 'Alice'
}
}
})
// ✅ 自动响应式,无需预先定义
state.user.profile.age = 25
state.user.address = { city: 'Beijing' } // 也自动响应式
10. Map/Set 集合类型
javascript
// Vue 2
data() {
return {
map: new Map() // ❌ Map 本身不是响应式
}
}
// Vue 3
const map = reactive(new Map()) // ✅ Map 操作也是响应式的
map.set('key', 'value')
console.log(map.get('key')) // 触发响应式更新
11. 类实例属性
javascript
// Vue 2
class User {
constructor(name) {
this.name = name
}
}
data() {
return {
user: new User('Alice') // ❌ 类实例属性非响应式
}
}
// Vue 3
class User {
constructor(name) {
this.name = name
}
}
const user = reactive(new User('Alice')) // ✅ 属性变为响应式
user.name = 'Bob' // 触发更新
12. 异步操作中的 this 指向
javascript
// Vue 2
methods: {
async fetchData() {
setTimeout(function() {
this.data = result // ❌ this 指向错误
}, 1000)
setTimeout(() => {
this.data = result // ✅ 箭头函数保持 this
}, 1000)
}
}
// Vue 3
setup() {
const data = ref(null)
const fetchData = async () => {
setTimeout(() => {
data.value = result // ✅ 箭头函数保持引用
}, 1000)
}
return { data, fetchData }
}
特殊场景对比
| 场景 | Vue 2 | Vue 3 | 说明 |
|---|---|---|---|
| 响应式对象赋值 | this.obj = newObj ❌ |
state.obj = newObj ✅ |
Vue 3 可以整体替换 |
| 条件添加属性 | this.$set 必须 |
直接赋值 ✅ | Vue 3 更简洁 |
| 对象冻结 | Object.freeze() 破坏响应式 |
同样破坏 | 两者都需要注意 |
| 异步数据更新 | 需用 this.$nextTick |
需用 nextTick |
机制相同 |
| v-for 索引更新 | 不推荐 | 支持但需注意 | Vue 3 支持更好 |
核心原则与最佳实践
Vue 2 记忆口诀
新增删除用 set/delete,数组方法用 splice,解构导出需谨慎
Vue 3 记忆口诀
reactive 配 toRefs,ref 解构用 toRef,直接赋值全响应
通用解决方案
-
保持引用:避免破坏响应式对象的引用链
-
使用工具函数 :
toRefs、toRef、computed -
提前定义结构:在 reactive 中预先定义所有需要的属性
-
使用 ref 包装:基础类型使用 ref,对象使用 reactive
代码检查清单
javascript
// ✅ 安全操作
const state = reactive({ count: 0, list: [] })
state.count++ // 修改属性
state.newProp = 'value' // 添加属性
delete state.count // 删除属性
state.list[0] = newValue // 修改数组项
state.list.length = 0 // 修改数组长度
// ⚠️ 需要小心
const { count } = toRefs(state) // 解构用 toRefs
const double = computed(() => state.count * 2) // 派生状态用 computed
// ❌ 危险操作
let { count } = state // 直接解构
let newState = { ...state } // 展开运算符
这种对比可以帮助开发者在实际开发中快速识别响应式丢失的场景,并选择合适的解决方案。
Vuex3,Vuex4和Pinia响应式丢失场景及解决方案
Vuex 3、Vuex 4 和 Pinia 响应式丢失场景及解决方案
| 响应式丢失场景 | Vuex 3 (Vue 2) | Vuex 4 (Vue 3) | Pinia (Vue 3) | 通用解决方案 |
|---|---|---|---|---|
| 1. 直接修改 state | ❌ 丢失响应式/不推荐 | ⚠️ 可修改但破坏追踪 | ✅ 允许直接修改 | Vuex: 使用 mutations/actions Pinia: 可直接修改或使用 $patch |
| 2. 动态添加 state 属性 | ❌ 需要 Vue.set | ⚠️ 部分支持 | ✅ 自动响应式 | Vuex: Vue.set(state, key, value) Pinia: 直接赋值 |
| 3. 动态删除 state 属性 | ❌ 需要 Vue.delete | ⚠️ 部分支持 | ✅ 自动响应式 | Vuex: Vue.delete(state, key) Pinia: delete state.key |
| 4. 解构 state | ❌ 丢失响应式 | ❌ 丢失响应式 | ⚠️ store 解构丢失,需用 storeToRefs | 使用 mapState、toRefs 或 storeToRefs |
| 5. 直接替换 state 对象 | ❌ 破坏响应式 | ❌ 破坏响应式 | ✅ 支持 $state 整体替换 |
Vuex: 逐个属性修改 Pinia: store.$state = newState |
| 6. 模块中的嵌套状态 | ⚠️ 需递归处理 | ⚠️ 需递归处理 | ✅ 自动响应式 | Vuex: 预先定义结构 Pinia: 自动处理 |
| 7. getters 中返回新对象 | ⚠️ 每次都重新计算 | ⚠️ 每次都重新计算 | ⚠️ 每次都重新计算 | 使用计算属性缓存,或返回 ref |
| 8. 数组操作 | ⚠️ 部分变异方法可用 | ⚠️ 部分变异方法可用 | ✅ 所有数组操作响应式 | Vuex: 使用变异方法 Pinia: 直接操作 |
| 9. 异步操作后更新 | ✅ 需 commit | ✅ 需 commit | ✅ 直接修改 | Pinia 更简洁,无需 commit |
| 10. 模块动态注册 | ⚠️ 需特殊处理 | ⚠️ 需特殊处理 | ✅ 支持动态 store | Vuex: registerModule Pinia: useStore 动态创建 |
| 11. 插件中修改 state | ⚠️ 需谨慎 | ⚠️ 需谨慎 | ✅ 更安全 | 遵循各状态管理库的插件规范 |
| 12. SSR 中的状态 | ⚠️ 需处理 | ⚠️ 需处理 | ✅ 内置支持 | Pinia 的 SSR 支持更好 |
详细说明与代码示例
1. 直接修改 state
javascript
// Vuex 3 (Vue 2)
// ❌ 直接修改(不推荐,破坏追踪)
this.$store.state.count = 10
// ✅ 正确方式
this.$store.commit('increment')
this.$store.dispatch('asyncIncrement')
// Vuex 4 (Vue 3)
// ⚠️ 可以直接修改但破坏 DevTools 追踪
store.state.count = 10
// ✅ 推荐方式
store.commit('increment')
// Pinia
// ✅ 允许直接修改
store.count = 10
// ✅ 更好的批量修改
store.$patch({ count: 10, name: 'Alice' })
store.$patch((state) => {
state.count = 10
state.name = 'Alice'
})
2-3. 动态添加/删除属性
javascript
// Vuex 3
const store = new Vuex.Store({
state: { user: {} }
})
// ❌ 直接添加
store.state.user.age = 25 // 非响应式
// ✅ 使用 Vue.set
Vue.set(store.state.user, 'age', 25)
// ❌ 直接删除
delete store.state.user.age // 非响应式
// ✅ 使用 Vue.delete
Vue.delete(store.state.user, 'age')
// Vuex 4
// ⚠️ 添加属性可能响应式,但不保证
store.state.user.age = 25
// ✅ 推荐重新赋值
store.state.user = { ...store.state.user, age: 25 }
// Pinia
const useStore = defineStore('main', {
state: () => ({ user: {} })
})
const store = useStore()
// ✅ 直接添加,完全响应式
store.user.age = 25
delete store.user.age // 也支持删除
4. 解构 state
javascript
// Vuex 3
// ❌ 直接解构丢失响应式
const { count, name } = this.$store.state
// ✅ 使用 mapState
computed: {
...mapState(['count', 'name'])
}
// ✅ 或使用辅助函数
computed: {
count() {
return this.$store.state.count
}
}
// Vuex 4 (Composition API)
import { computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// ❌ 直接解构丢失响应式
const { count } = store.state
// ✅ 使用 computed
const count = computed(() => store.state.count)
const name = computed(() => store.state.name)
// Pinia
import { storeToRefs } from 'pinia'
const store = useStore()
// ❌ 直接解构丢失响应式
const { count, name } = store
// ✅ 使用 storeToRefs
const { count, name } = storeToRefs(store)
// ✅ 可以直接使用 store 的属性(模板中)
// 但在 JS 中需要 .value
console.log(count.value)
// ✅ actions 可以直接解构
const { increment } = store
5. 直接替换 state 对象
javascript
// Vuex 3 & 4
// ❌ 直接替换整个 state 对象
store.state = { count: 0, name: 'Alice' } // 破坏响应式
// ✅ 逐个属性替换
Object.assign(store.state, { count: 0, name: 'Alice' })
// ✅ 或使用 mutation
mutations: {
replaceState(state, newState) {
Object.assign(state, newState)
}
}
// Pinia
const store = useStore()
// ✅ 支持整体替换
store.$state = { count: 0, name: 'Alice' }
// ✅ 也可以使用 $patch
store.$patch({ count: 0, name: 'Alice' })
6. 模块中的嵌套状态
javascript
// Vuex 3
const store = new Vuex.Store({
modules: {
user: {
state: {
profile: {
name: 'Alice'
}
}
}
}
})
// ❌ 动态添加深层属性
store.state.user.profile.age = 25 // 非响应式
// ✅ 使用 Vue.set
Vue.set(store.state.user.profile, 'age', 25)
// Pinia
const useUserStore = defineStore('user', {
state: () => ({
profile: {
name: 'Alice'
}
})
})
const userStore = useUserStore()
// ✅ 自动响应式
userStore.profile.age = 25
userStore.profile.address = { city: 'Beijing' } // 深层也响应式
7. getters 中返回新对象
javascript
// Vuex 3 & 4
const store = new Vuex.Store({
state: {
items: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
},
getters: {
// ⚠️ 每次都返回新数组
filteredItems: (state) => {
return state.items.filter(item => item.id > 1)
},
// ✅ 返回缓存的计算结果
cachedFilter: (state) => {
return computed(() => state.items.filter(item => item.id > 1))
}
}
})
// Pinia
const useStore = defineStore('main', {
state: () => ({
items: [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
}),
getters: {
// ⚠️ 仍然每次都重新计算
filteredItems: (state) => {
return state.items.filter(item => item.id > 1)
},
// ✅ 返回计算属性(缓存)
filteredItemsComputed: (state) => {
return computed(() => state.items.filter(item => item.id > 1))
}
}
})
8. 数组操作
javascript
// Vuex 3 & 4
const store = new Vuex.Store({
state: {
list: [1, 2, 3]
},
mutations: {
// ✅ 使用数组变异方法
updateList(state) {
state.list.push(4) // 响应式
state.list.splice(0, 1) // 响应式
},
// ❌ 通过索引修改(Vuex 3 非响应式)
badUpdate(state) {
state.list[0] = 100 // Vuex 3 非响应式
}
}
})
// Pinia
const store = useStore()
// ✅ 所有数组操作都响应式
store.list.push(4)
store.list[0] = 100
store.list.length = 0
store.list.splice(0, 1)
9. 异步操作后更新
javascript
// Vuex 3 & 4
const store = new Vuex.Store({
state: { data: null },
mutations: {
setData(state, data) {
state.data = data
}
},
actions: {
async fetchData({ commit }) {
const data = await api.getData()
commit('setData', data) // 必须 commit
}
}
})
// Pinia
const useStore = defineStore('main', {
state: () => ({ data: null }),
actions: {
async fetchData() {
const data = await api.getData()
this.data = data // 直接修改,无需 commit
}
}
})
10. 模块动态注册
javascript
// Vuex 3
// 动态注册模块
store.registerModule('newModule', {
state: { count: 0 }
})
// ⚠️ 模块中的状态需要特殊处理
store.state.newModule.count = 10 // 可能非响应式
// Pinia
// ✅ 动态创建 store
const useDynamicStore = defineStore('dynamic', {
state: () => ({ count: 0 })
})
const dynamicStore = useDynamicStore() // 自动注册
dynamicStore.count = 10 // 完全响应式
11-12. 插件和 SSR
javascript
// Pinia 插件示例
const piniaPlugin = (context) => {
const { store } = context
// ✅ 安全修改 state
store.$patch({ extra: 'data' })
// ✅ 订阅状态变化
store.$subscribe((mutation, state) => {
console.log('State changed', state)
})
}
// SSR 中使用
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)
// 服务端预取数据
if (import.meta.env.SSR) {
const store = useStore()
await store.fetchData()
}
对比总结表
| 特性 | Vuex 3 | Vuex 4 | Pinia |
|---|---|---|---|
| 响应式原理 | Object.defineProperty | Proxy | Proxy |
| 直接修改 state | ❌ 不推荐 | ⚠️ 可修改但破坏追踪 | ✅ 推荐 |
| TypeScript 支持 | ⚠️ 较弱 | ⚠️ 一般 | ✅ 优秀 |
| 模块化 | ✅ 原生支持 | ✅ 原生支持 | ✅ 更简洁 |
| 动态添加属性 | ❌ 需要 Vue.set | ⚠️ 部分支持 | ✅ 自动响应式 |
| 解构保持响应式 | 需要 mapState | 需要 computed | 需要 storeToRefs |
| 代码量 | 较多 | 较多 | 较少 |
| 学习曲线 | 陡峭 | 陡峭 | 平缓 |
| DevTools | ✅ 成熟 | ✅ 成熟 | ✅ 现代化 |
| 体积 | ~10KB | ~10KB | ~2KB |
最佳实践建议
Vuex 3
javascript
// ✅ 始终通过 mutations 修改
mutations: {
updateUser(state, payload) {
Vue.set(state.user, 'age', payload.age)
}
}
// ✅ 使用 mapState 保持响应式
computed: {
...mapState(['user', 'count'])
}
Vuex 4
javascript
// ✅ 使用 Composition API
import { computed } from 'vue'
const store = useStore()
const count = computed(() => store.state.count)
// ✅ 批量更新使用 Object.assign
mutations: {
updateState(state, payload) {
Object.assign(state, payload)
}
}
Pinia
javascript
// ✅ 推荐:直接修改 + $patch
const store = useStore()
store.count++
store.$patch({ name: 'Alice' })
// ✅ 解构时使用 storeToRefs
const { count, name } = storeToRefs(store)
// ✅ actions 中使用 this
actions: {
async updateData() {
const data = await fetch()
this.data = data // 直接修改
}
}
迁移指南
Vuex 4 → Pinia 响应式相关迁移
javascript
// Vuex 4
const store = new Vuex.Store({
state: { count: 0, user: {} },
mutations: {
increment(state) {
state.count++
},
setUser(state, user) {
Vue.set(state, 'user', user) // 需要特殊处理
}
},
actions: {
async fetchUser({ commit }) {
const user = await api.getUser()
commit('setUser', user)
}
}
})
// Pinia
const useStore = defineStore('main', {
state: () => ({ count: 0, user: {} }),
actions: {
increment() {
this.count++ // 直接修改
},
async fetchUser() {
const user = await api.getUser()
this.user = user // 直接赋值,自动响应式
}
}
})
核心要点:
-
Vuex 3/4:响应式更新需要通过 mutations,动态属性需要特殊处理
-
Pinia:完全响应式,直接修改即可,更符合直觉
-
迁移建议:从 Vuex 迁移到 Pinia 时,可以大幅简化状态更新逻辑
Vue 2/3 及状态管理库开发注意事项完整总结
| 注意事项分类 | Vue 2 | Vue 3 | Vuex 3 | Vuex 4 | Pinia |
|---|---|---|---|---|---|
| 生命周期钩子 | beforeCreate, created beforeMount, mounted beforeUpdate, updated beforeDestroy, destroyed |
setup 替代 beforeCreate/created onBeforeMount, onMounted onBeforeUpdate, onUpdated onBeforeUnmount, onUnmounted |
beforeCreate/created 中可访问 |
setup 中通过 useStore() |
setup 中使用,支持组合式 API |
| 组件通信 | $emit, $on, $off, $parent $children, $refs, provide/inject |
emit, props, $refs provide/inject, mitt 替代事件总线 |
dispatch, commit mapActions, mapMutations |
同 Vuex 3 | store 直接调用 store.$patch |
| 响应式数据定义 | data() 返回对象 Vue.set 添加属性 |
ref, reactive, toRefs computed, readonly |
state 函数返回对象 |
同 Vuex 3 | state 函数返回对象 支持 ref/reactive |
| 模板语法限制 | 每个组件只能一个根元素 | 支持多个根元素(Fragment) | N/A | N/A | N/A |
| 异步组件 | () => import('./Comp.vue') Vue.component('async', resolve) |
defineAsyncComponent 支持 Suspense |
import() 动态导入模块 |
同 Vuex 3 | 支持动态 store 导入 |
| TypeScript 支持 | ⚠️ 类型推断弱 需要 vue-class-component |
✅ 优秀的内置支持 完善的类型推断 | ⚠️ 需要额外类型定义 | ⚠️ 部分支持 | ✅ 优秀的内置支持 |
| 性能优化 | keep-alive, v-once v-show, 异步组件 Object.freeze |
v-memo, v-once shallowRef, shallowReactive Tree-shaking 优化 |
模块懒加载 命名空间模块 | 同 Vuex 3 | 自动 tree-shaking 按需加载 |
| 内存泄漏风险 | 全局事件总线 未销毁的定时器 未解绑的 DOM 事件 | 同 Vue 2 但组合式 API 更易清理 | 未注销的模块 大量状态积累 | 同 Vuex 3 | 自动清理未使用的 store |
| SSR 注意事项 | 避免浏览器 API 使用 created 而非 mounted 使用 this.$isServer |
使用 onMounted 判断 <ClientOnly> 组件 useSSRContext |
避免在 actions 中使用浏览器 API | 同 Vuex 3 | 内置 SSR 支持 $subscribe 在 SSR 中谨慎使用 |
| 打包体积优化 | 完整版 32KB 运行时版 22KB | 支持 tree-shaking 按需引入 API | 完整版 ~10KB | 同 Vuex 3 | ~2KB 按需引入 |
| 调试工具 | Vue Devtools 成熟 | Vue Devtools 功能更强 | Vue Devtools 集成 | Vue Devtools 集成 | Pinia Devtools 专用 |
| 测试便利性 | 需要 mock 很多 API @vue/test-utils |
组合式 API 易测试 @vue/test-utils 改进 |
需要 mock store | 同 Vuex 3 | 易于 mock 和测试 |
| 国际化 (i18n) | vue-i18n@8 $t 全局方法 |
vue-i18n@9 Composition API 支持 |
需集成到 store | 同 Vuex 3 | 集成简单 |
| 路由集成 | vue-router@3 $route, $router |
vue-router@4 useRoute, useRouter |
路由守卫中使用 store | 同 Vuex 3 | 路由守卫中直接使用 |
| 错误处理 | errorCaptured Vue.config.errorHandler |
errorCaptured app.config.errorHandler |
插件中的错误处理 | 同 Vuex 3 | 插件中的错误处理 $onAction 错误捕获 |
详细说明与代码示例
1. 生命周期钩子
html
<!-- Vue 2 -->
<script>
export default {
data() {
return { count: 0 }
},
beforeCreate() {
console.log('实例初始化前')
},
created() {
console.log('实例创建完成')
},
beforeMount() {
console.log('挂载前')
},
mounted() {
console.log('挂载完成')
},
beforeDestroy() {
console.log('销毁前')
},
destroyed() {
console.log('销毁完成')
}
}
</script>
<!-- Vue 3 -->
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
const count = ref(0)
onBeforeMount(() => {
console.log('挂载前')
})
onMounted(() => {
console.log('挂载完成')
})
onBeforeUnmount(() => {
console.log('卸载前')
})
onUnmounted(() => {
console.log('卸载完成')
})
</script>
2. 组件通信
javascript
// Vue 2 通信方式
// 1. props/$emit
this.$emit('update', data)
// 2. 事件总线
Vue.prototype.$bus = new Vue()
this.$bus.$on('event', handler)
this.$bus.$emit('event', data)
// 3. $parent/$children
this.$parent.method()
this.$children[0].method()
// 4. provide/inject
provide() { return { data: this.data } }
inject: ['data']
// Vue 3 通信方式
// 1. props/emit
const emit = defineEmits(['update'])
emit('update', data)
// 2. mitt (替代事件总线)
import mitt from 'mitt'
const emitter = mitt()
emitter.on('event', handler)
emitter.emit('event', data)
// 3. provide/inject
provide('key', ref(data))
const data = inject('key')
// 4. $refs (Composition API)
const childRef = ref()
childRef.value.method()
3. 响应式数据定义
javascript
// Vue 2
export default {
data() {
return {
count: 0,
user: { name: 'Alice' }
}
},
methods: {
addProp() {
// ❌ 错误:不会响应式
this.user.age = 25
// ✅ 正确
this.$set(this.user, 'age', 25)
}
}
}
// Vue 3
import { ref, reactive, toRefs } from 'vue'
const count = ref(0) // 基础类型
const state = reactive({ // 对象类型
count: 0,
user: { name: 'Alice' }
})
// ✅ 自动响应式
state.user.age = 25
// 解构保持响应式
const { user } = toRefs(state)
4. 模板语法限制
html
<!-- Vue 2: 必须有根元素 -->
<template>
<div> <!-- 必须有一个根元素 -->
<h1>Title</h1>
<p>Content</p>
</div>
</template>
<!-- Vue 3: 支持多根元素 -->
<template>
<h1>Title</h1>
<p>Content</p> <!-- 不需要包裹 -->
</template>
5. 异步组件
javascript
// Vue 2
Vue.component('async-comp', () => import('./Comp.vue'))
// 或
const AsyncComp = () => import('./Comp.vue')
// Vue 3
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./Comp.vue')
)
// 高级用法
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
loadingComponent: LoadingComp,
errorComponent: ErrorComp,
delay: 200,
timeout: 3000
})
6. TypeScript 支持
TypeScript
// Vue 2 (需要装饰器)
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export default class MyComp extends Vue {
count: number = 0
increment(): void {
this.count++
}
}
// Vue 3 (原生支持)
<script setup lang="ts">
import { ref } from 'vue'
interface User {
name: string
age: number
}
const count = ref<number>(0)
const user = ref<User>({ name: 'Alice', age: 25 })
</script>
// Pinia TypeScript
interface MainState {
count: number
user: User | null
}
export const useMainStore = defineStore('main', {
state: (): MainState => ({
count: 0,
user: null
}),
actions: {
async fetchUser(id: number): Promise<void> {
// 完全类型推断
this.user = await api.getUser(id)
}
}
})
7. 性能优化
javascript
// Vue 2 优化
<template>
<div>
<!-- 静态内容只渲染一次 -->
<div v-once>{{ staticContent }}</div>
<!-- 列表优化 -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
</div>
</template>
<script>
export default {
computed: {
// 冻结不需要响应式的数据
frozenData() {
return Object.freeze(largeData)
}
}
}
</script>
// Vue 3 优化
<script setup>
import { shallowRef, shallowReactive, vMemo } from 'vue'
// 浅响应式,优化大对象
const shallowState = shallowReactive({
largeData: { /* 大量数据 */ }
})
// 浅 ref
const shallowArray = shallowRef([1, 2, 3])
// 使用 v-memo 缓存模板片段
</script>
<template>
<div v-memo="[item.id]">
<!-- 只有 item.id 变化时才重新渲染 -->
<p>{{ item.name }}</p>
</div>
</template>
8. 内存泄漏预防
javascript
// Vue 2 预防
export default {
data() {
return {
timer: null
}
},
mounted() {
// 定时器
this.timer = setInterval(() => {}, 1000)
// 全局事件
window.addEventListener('resize', this.handleResize)
// 事件总线
this.$bus.$on('event', this.handler)
},
beforeDestroy() {
// 清理
clearInterval(this.timer)
window.removeEventListener('resize', this.handleResize)
this.$bus.$off('event', this.handler)
}
}
// Vue 3 预防
<script setup>
import { onMounted, onUnmounted } from 'vue'
let timer = null
const handleResize = () => {}
onMounted(() => {
timer = setInterval(() => {}, 1000)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
clearInterval(timer)
window.removeEventListener('resize', handleResize)
})
</script>
9. SSR 注意事项
javascript
// Vue 2 SSR
export default {
mounted() {
// ✅ 只在客户端执行
if (!this.$isServer) {
this.clientOnlyMethod()
}
},
created() {
// ❌ 避免在 created 中使用浏览器 API
// window.location 会报错
}
}
// Vue 3 SSR
<script setup>
import { onMounted } from 'vue'
// ✅ 使用 onMounted,只在客户端执行
onMounted(() => {
document.title = 'Title'
})
// ✅ 使用 ClientOnly 组件
</script>
<template>
<ClientOnly>
<ClientComponent />
</ClientOnly>
</template>
10. 状态管理注意事项
javascript
// Vuex 3/4 注意事项
const store = new Vuex.Store({
state: {
count: 0,
user: null
},
mutations: {
// ✅ 同步操作
increment(state) {
state.count++
},
// ❌ 不要在 mutation 中做异步
asyncBadMutation(state) {
const data = await fetch() // 错误
}
},
actions: {
// ✅ 异步操作放在 actions
async fetchUser({ commit }) {
const user = await api.getUser()
commit('setUser', user)
}
}
})
// Pinia 注意事项
const useStore = defineStore('main', {
state: () => ({
count: 0,
user: null
}),
actions: {
// ✅ 异步操作直接在 actions
async fetchUser() {
const user = await api.getUser()
this.user = user // 直接修改
},
// ✅ 支持直接异步
async incrementAsync() {
await delay(1000)
this.count++
}
},
// ⚠️ getters 不能是异步的
getters: {
// ❌ 错误:getter 不能异步
asyncData: (state) => {
return await fetch() // 错误
}
}
})
开发检查清单
Vue 2 特有检查项
-
是否使用了
$set添加新属性 -
模板是否有唯一根元素
-
是否清理了全局事件总线
-
异步组件是否正确注册
Vue 3 特有检查项
-
是否正确使用
ref/reactive -
解构时是否使用
toRefs/storeToRefs -
是否利用
shallowRef优化大对象 -
是否正确使用组合式 API 生命周期
状态管理通用检查项
-
Vuex 3/4: mutations 是否都是同步的
-
Vuex 3/4: 动态属性是否使用
Vue.set -
Pinia: 解构是否使用
storeToRefs -
Pinia: 是否正确使用
$patch批量更新 -
模块是否正确命名空间(Vuex)
-
是否避免了在 getters 中产生副作用
性能检查项
-
大列表是否使用
v-once或v-memo -
是否按需引入组件和库
-
是否正确使用
keep-alive -
路由是否懒加载
-
是否避免了不必要的响应式数据
迁移注意事项对照表
| 迁移场景 | Vue 2 → Vue 3 | Vuex 3 → Vuex 4 | Vuex → Pinia |
|---|---|---|---|
| 生命周期 | 钩子函数改为组合式 API | 基本不变 | 使用组合式 API |
| 响应式 | data → ref/reactive |
基本不变 | state 支持 ref/reactive |
| 组件通信 | 移除 $on/$off/$once |
基本不变 | 直接调用 store 方法 |
| 类型支持 | 需要装饰器 → 原生 TS | 类型增强 | 完美的 TS 支持 |
| 模块化 | 基本不变 | 基本不变 | 更简洁的模块系统 |
| 代码量 | 减少 30-50% | 基本不变 | 减少 40-60% |
核心开发原则:
-
Vue 3: 优先使用组合式 API,充分利用 Proxy 响应式
-
Pinia: 简单直接的状态管理,避免过度设计
-
性能: 合理使用浅响应式 API 优化大对象
-
内存: 及时清理副作用和事件监听
-
类型: 充分利用 TypeScript 的类型推断