Vue 计算属性和侦听器详解
计算属性(computed)和侦听器(watch)是 Vue 响应式系统中两个核心概念,它们都以不同的方式响应数据变化。下面我将全面解析它们的原理、使用场景和最佳实践。
一、计算属性 (Computed)
1.1 基本概念
计算属性是基于它们的响应式依赖进行缓存的派生值,只有当依赖发生变化时才会重新计算。
js
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
1.2 特点
- 缓存机制:依赖不变时直接返回缓存值
- 惰性求值 :只有被访问时才会计算
- 响应式:自动追踪依赖关系
1.3 完整语法
js
const userInfo = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
1.4 计算属性缓存原理
Vue 内部实现简化的依赖追踪机制:
js
function computed(getter) {
let value;
let dirty = true; // 标记是否需要重新计算
const runner = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(this, 'value') // 触发依赖更新
}
})
return {
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(this, 'value') // 收集依赖
return value
}
}
}
二、侦听器 (Watch)
2.1 基本概念
侦听器用于在数据变化时执行副作用操作,比计算属性更适合执行异步或开销较大的操作。
js
watch(count, (newVal, oldVal) => {
console.log(`count变化: ${oldVal} -> ${newVal}`)
})
2.1.1 高级配置选项
js
watch(source, callback, {
immediate: true, // 立即执行
deep: true, // 深度监听
flush: 'post', // DOM更新后触发
onTrack(e) { // 调试依赖追踪
debugger
},
onTrigger(e) { // 调试触发更新
debugger
}
})
2.2 watch API 变体
2.2.1 侦听单个源
js
// ref
watch(count, callback)
// getter 函数
watch(() => state.count, callback)
// 响应式对象属性
watch(() => obj.prop, callback)
2.2.2 侦听多个源
js
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
2.2.3 watchEffect
自动追踪依赖的即时回调:
js
watchEffect(() => {
console.log('count:', count.value)
})
特点:
- 自动收集依赖
- 立即执行
- 无需指定侦听源
2.3 watch中的陷阱 (避坑指南)
2.3.1. Ref
当使用 ref
包裹一个对象时,Vue 的响应式系统仍然能够监听到对象内部的变化,但有一些特定的行为需要注意。
1. ref
包裹对象的工作原理
js
const objRef = ref({
name: 'Alice',
age: 25
})
实际上等价于:
js
const objRef = ref(reactive({
name: 'Alice',
age: 25
}))
Vue 会自动用 reactive()
包裹对象值,所以:
- 通过
.value
访问的是 reactive 代理对象 - 对象内部的修改会被追踪
2. 监听对象内部变化
js
watch(objRef, (newVal, oldVal) => {
console.log('对象变化:', newVal)
}, { deep: true }) // Vue 3.4+ 可以省略 deep: true
可以监听到:
- 添加/删除属性
- 修改嵌套属性
- 数组变化
3.监听方式对比
- 监听整个 ref 对象
js
// 方式1:监听整个ref(需要.value访问)
watch(() => objRef.value, (newVal) => {
console.log('变化:', newVal)
}, { deep: true })
// 方式2:直接解包(Vue 3.3+)
watch(objRef, (newVal) => {
console.log('变化:', newVal)
}, { deep: true })
- 监听特定属性
js
// 监听特定属性(不需要deep)
watch(() => objRef.value.name, (newName) => {
console.log('名字变化:', newName)
})
4.特殊情况处理
- 替换整个对象
js
// 替换整个对象(会触发响应)
objRef.value = { name: 'Bob', age: 30 }
// 监听会触发,且能获取正确的oldVal
- 解构问题
js
// 错误!失去响应性
const { name, age } = objRef.value;
// 正确保持响应性
const name = computed(() => objRef.value.name);
const { name } = toRefs(objRef.value);
2.3.2. Reactive
在 Vue 3 的响应式系统中,当你在 watch
中使用 reactive
对象时,会有一些特定的行为和注意事项。
js
// 推荐 - 只侦听需要的属性
watch(() => state.importantProp, callback)
// 推荐 - 侦听多个属性
watch([() => state.a, () => state.b], ([a, b]) => {})
// 必要时才侦听整个对象
watch(state, callback, { deep: true }) // Vue 3.4+ 可省略 deep
1. 直接侦听整个 reactive 对象
js
const state = reactive({ count: 0, user: { name: 'Alice' } })
// 侦听整个 reactive 对象
watch(state, (newVal, oldVal) => {
console.log('state changed:', newVal)
})
行为特点:
- 任何嵌套属性的变化都会触发回调
newVal
和oldVal
将是相同的引用(因为 reactive 对象是引用类型,所以 newVal === oldVal )- 需要
deep: true
才能正常工作(Vue 3.4+ 已默认启用)
2. 侦听 reactive 对象的特定属性
js
watch(() => state.count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal)
})
行为特点:
- 只有特定属性变化才会触发
- 可以正确获取
oldVal
- 不需要
deep
选项
3.新旧值相同的问题
当侦听整个 reactive 对象时,newVal
和 oldVal
会是相同的:
js
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal) // true
})
原因:reactive 对象是引用类型,Vue 不会对其进行深拷贝
解决方案:
-
侦听特定属性
-
手动深拷贝旧值:
jswatch(() => ({ ...state }), (newVal, oldVal) => { console.log(newVal === oldVal) // false }, { deep: true })
三、计算属性 vs 侦听器
特性 | 计算属性 | 侦听器 |
---|---|---|
目的 | 派生新数据 | 执行副作用 |
缓存 | 有缓存 | 无缓存 |
返回值 | 必须返回 | 不需要返回 |
异步 | 不支持 | 支持 |
初始化 | 惰性求值 | 可配置 immediate |
依赖追踪 | 自动 | 显式指定 |
适用场景 | 模板渲染、数据转换 | API调用、DOM操作 |
四、最佳实践
4.1 计算属性最佳实践
- 纯函数:避免副作用
- 简单计算:复杂逻辑考虑拆分
- 命名语义化 :如
fullName
、isValid
- 避免修改依赖:保持单向数据流
js
// 好的实践
const discountedPrice = computed(() => {
return basePrice.value * (1 - discount.value)
})
// 不好的实践 - 有副作用
const badComputed = computed(() => {
fetchData() // 副作用操作
return ...
})
4.2 侦听器最佳实践
- 明确依赖:避免过度使用 deep
- 防抖节流:高频操作优化
- 清理副作用:返回清理函数
- 避免无限循环:注意修改侦听的数据
js
// 带防抖的搜索
watch(searchQuery, debounce((query) => {
fetchResults(query)
}, 500))
// 清理副作用示例
watch(data, (newVal) => {
const timer = setInterval(() => {
syncToServer(newVal)
}, 1000)
return () => clearInterval(timer)
})
五、性能优化
5.1 计算属性优化
- 减少依赖:只依赖必要的数据
- 避免复杂计算:大数组操作考虑预处理
- 使用
v-memo
:配合计算属性优化渲染
js
const bigList = computed(() => {
// 使用 Map 优化查找性能
const map = new Map()
rawList.value.forEach(item => map.set(item.id, item))
return map
})
5.2 侦听器优化
- 避免深度监听:明确指定嵌套路径
- 惰性监听 :使用
{ lazy: true }
选项 - 分离监听器:不同逻辑分开监听
js
// 优化前 - 深度监听整个对象
watch(obj, callback, { deep: true })
// 优化后 - 只监听需要的属性
watch(() => obj.importantProp, callback)
六、实际应用场景
6.1 计算属性典型场景
- 数据格式化
js
const formattedDate = computed(() => {
return new Date(date.value).toLocaleString()
})
- 过滤/排序列表
js
const filteredUsers = computed(() => {
return users.value.filter(u => u.active)
})
- 条件显示
js
const showButton = computed(() => {
return user.value.role === 'admin' && items.value.length > 0
})
6.2 侦听器典型场景
- API调用
js
watch(route.params.id, (newId) => {
fetchUser(newId)
})
- 表单验证
js
watch(() => form.username, (newVal) => {
validateUsername(newVal)
})
- 路由参数监听
js
watch(() => route.params.id, (newId) => {
fetchUserDetails(newId)
}, { immediate: true })
- 本地存储同步
js
watch(todos, (newTodos) => {
localStorage.setItem('todos', JSON.stringify(newTodos))
}, { deep: true })
七、原理深入
7.1 计算属性实现机制
Vue 计算属性基于响应式系统和调度器实现:
- 首次访问:执行计算函数并缓存结果
- 依赖变化:标记缓存失效 (dirty = true)
- 再次访问:重新计算并更新缓存
- 无变化:直接返回缓存值
7.2 侦听器实现机制
Vue 侦听器基于 effect 和调度器:
- 初始化:创建 effect 并执行一次 (除非 lazy)
- 依赖变化:触发调度器
- 调度执行:根据 flush 时机执行回调
- 清理:组件卸载时自动清理
八、常见问题
8.1 计算属性 vs 方法
- 计算属性:基于依赖缓存,适合派生数据
- 方法:每次重新执行,适合事件处理
js
// 计算属性 - 高效
const fullName = computed(() => `${firstName} ${lastName}`)
// 方法 - 每次重新计算
function getFullName() {
return `${firstName} ${lastName}`
}
8.2 watch vs watchEffect
- watch:需要显式指定源,更精确控制
- watchEffect:自动收集依赖,更简洁
js
// watch - 明确指定依赖
watch(() => state.count, (count) => {
console.log(count)
})
// watchEffect - 自动追踪
watchEffect(() => {
console.log(state.count)
})
8.3 为什么计算属性要有缓存?
- 性能优化:避免重复计算
- 一致性:保证多次访问同一值
- 避免副作用:防止意外多次执行
总结
计算属性和侦听器是 Vue 响应式系统的两大支柱:
- 优先使用计算属性:用于派生数据和模板渲染
- 合理使用侦听器:用于副作用操作和异步任务
- 注意性能影响:避免不必要的重新计算和深度监听
- 理解原理:有助于编写更高效的代码
掌握它们的区别和使用场景,可以显著提升 Vue 应用的性能和可维护性。