一、计算属性(computed)
 
计算属性(Computed Properties)是 Vue 中一种特殊的响应式数据,它能基于已有的响应式数据动态计算出新的数据。
计算属性有以下特性:
-  
自动缓存:只有当它依赖的响应式数据发生变化时,才会重新计算。
 -  
响应式更新:依赖的数据变化后,会自动触发计算属性重新计算。
 -  
简化模板:在模板中使用计算属性可以减少复杂逻辑,让模板更清晰、易读。
 
简单来说:
计算属性是基于其他响应式数据而自动计算得到的值,且具有缓存和响应式的特性。
1、计算属性的基本用法
<script setup>
import { ref, computed } from 'vue'// 响应式数据
const firstName = ref('Tom')
const lastName = ref('Jerry')// 计算属性(根据响应式数据动态计算)
const fullName = computed(() => {return `${firstName.value} ${lastName.value}`
})
</script><template><div>{{ fullName }}</div> <!-- 显示:Tom Jerry -->
</template>
 
注意默认计算属性是只读的,但也可以定义成可写。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
<script setup>
import { ref, computed } from 'vue'const firstName = ref('John')
const lastName = ref('Doe')const fullName = computed({// getterget() {return firstName.value + ' ' + lastName.value},// setterset(newValue) {// 注意:我们这里使用的是解构赋值语法[firstName.value, lastName.value] = newValue.split(' ')}
})
</script> 
 现在当你再运行 fullName.value = 'John Doe' 时,setter 会被调用而 firstName 和 lastName 会随之更新。
注意,computed()里面不接受任何的参数,我们看到里面有一个回调函数,这个回调函数本质上是getter函数
-  
之前版本(<3.4),这个 getter 函数没有参数。
 -  
从 3.4 开始,这个 getter 函数可以接受一个参数:就是上一次计算属性的计算结果。
 
简单来说:
如果需要,可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值:
<script setup>
import { ref, computed } from 'vue'const count = ref(2)// 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
// 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
// 直到 count 的值再次小于或等于 3 为止。
const alwaysSmall = computed((previous) => {if (count.value <= 3) {return count.value}return previous
})
</script> 
如果你正在使用可写的计算属性的话:
<script setup>
import { ref, computed } from 'vue'const count = ref(2)const alwaysSmall = computed({get(previous) {if (count.value <= 3) {return count.value}return previous},set(newValue) {count.value = newValue * 2}
})
</script> 
2、计算属性与方法(methods)的详细区别
两者区别如下:
| 对比维度 | 计算属性(computed) | 方法(methods) | 
|---|---|---|
| 缓存机制 | 有缓存,仅数据变化才重新计算 | 无缓存,每次调用都会执行 | 
| 调用方式 | 不需要括号调用,像属性一样使用 | 需要括号调用,明确为函数 | 
| 适用场景 | 基于响应式数据的计算 | 处理事件或显式调用的场景 | 
| 性能开销 | 性能较高(缓存优化) | 性能较低(频繁调用时) | 
计算属性与方法性能差异分析
假设模板多次渲染对比:
-  
计算属性:
<div>{{ doubleCount }}</div> <div>{{ doubleCount }}</div> <div>{{ doubleCount }}</div>-  
只计算一次,缓存结果。
 
 -  
 -  
方法调用:
<div>{{ getDoubleCount() }}</div> <div>{{ getDoubleCount() }}</div> <div>{{ getDoubleCount() }}</div>-  
每次都调用一次,共调用3次,性能浪费。
 
 -  
 
因此,对于频繁使用但数据不频繁变化的场景,建议使用计算属性。
3、计算属性什么时候不能用?
计算属性适用于:
-  
同步、快速的计算逻辑。
 -  
无副作用的计算(纯函数)。
 
计算属性不适合:
-  
异步逻辑(如请求数据)。
 -  
执行副作用(修改其他数据、DOM 操作)。
 
二、监听属性
在 Vue 中,监听属性(Watcher) 是一种响应式机制,用于监测响应式数据的变化:
-  
当你想在数据发生变化时执行某些逻辑(如发送请求、更新数据或执行某些副作用)时,就可以使用监听属性。
 -  
监听属性通过 Vue 提供的
watch()或watchEffect()函数实现。 
简单来说:
监听属性让你能够对数据变化做出反应,执行一些副作用或异步操作。
1、监听属性的基本用法(watch)
<script setup>
import { ref, watch } from 'vue'const count = ref(0)// 监听 count 的变化
watch(count, (newValue, oldValue) => {console.log(`count变化了:从${oldValue}到${newValue}`)
})
</script><template><button @click="count++">增加 ({{ count }})</button>
</template>
 
当 count 的值改变时,watch 会自动触发,执行回调函数。
监听多个数据:
const firstName = ref('Tom')
const lastName = ref('Jerry')// 同时监听 firstName 和 lastName
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {console.log(`名字变化了:${oldFirst} ${oldLast} => ${newFirst} ${newLast}`)
})
 
2、监听属性的参数与选项(高级用法)
🔹 监听属性的函数签名:watch(source, callback, options)
-  
source: 需要监听的响应式数据,可以是单个或多个。可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组: -  
callback: 数据变化时执行的回调函数。 -  
options: 可选参数,控制监听器行为。 
🔹 常用的监听选项(options):
| 选项 | 含义 | 默认值 | 
|---|---|---|
immediate | 是否立即执行一次监听回调 | false | 
deep | 是否深度监听对象内部属性变化 | false | 
flush | 控制监听器回调的执行时机 | 'pre' | 
深度监听(deep)到底是什么?
-  
默认情况下,Vue 的监听器只能监听对象引用本身的变化(比如替换对象)。
 -  
使用
deep: true时,能监听对象或数组内部属性或元素的变化。 
const user = ref({ name: 'Tom', age: 18 })// 默认浅监听(只能监听整个对象变化)
watch(user, () => {console.log('浅监听:user变化了')
})user.value.age = 19 // ❌不会触发浅监听(对象引用未变)
user.value = { name: 'Jerry', age: 19 } // ✔️触发// 深度监听(对象内部属性变化也会触发)
watch(user, () => {console.log('深监听:user变化了')
}, { deep: true })user.value.age = 20 // ✔️触发深度监听
 
监听属性的执行时机(flush)
flush 控制监听回调的执行时机:
| flush 值 | 含义 | 使用场景 | 
|---|---|---|
pre | 默认值,组件更新之前执行 | 大多数情况 | 
post | 组件更新之后执行 | 需要访问更新后的DOM时 | 
sync | 同步触发,数据变化立即执行 | 非常特殊情况 | 
3、watch vs watchEffect 的区别
在 Vue 中,watch() 和 watchEffect() 都用于响应式地执行一些副作用操作(如发起请求、改变 DOM),但二者的追踪数据依赖方式不同:
| 特性 | watch() | watchEffect() | 
|---|---|---|
| 如何追踪依赖 | 手动显式指定要监听的数据(明确) | 自动追踪回调中访问的数据(隐式) | 
| 首次执行 | 默认不立即执行,需手动开启 | 自动立即执行一次 | 
| 控制粒度 | 精确控制监听的数据项,控制更细 | 自动追踪所有访问的响应式数据,更灵活 | 
| 适用场景 | 明确知道监听什么数据变化 | 数据依赖较多或复杂,更希望自动追踪 | 
举个简单例子,监听单个明确的数据:
const todoId = ref(1)
const data = ref(null)watch(todoId,async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()},{ immediate: true }
)
 
特点:
-  
todoId被显式声明为监听的源。 -  
回调函数只在明确的源数据(
todoId)改变时触发。 -  
必须用
{ immediate: true }来立即执行一次,否则首次不会执行。 
1、 watchEffect() 如何简化上面的例子?
 
const todoId = ref(1)
const data = ref(null)watchEffect(async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()
})
 
特点:
-  
自动立即执行一次,无需手动指定
{ immediate: true }。 -  
无需手动指定监听源,回调函数内的所有响应式数据访问(这里是
todoId.value)会自动被 Vue 跟踪。 -  
一旦被跟踪的数据变化(例如:
todoId.value改变),回调会自动再次执行。 
 2、watchEffect() 的依赖跟踪原理(关键):
 
watchEffect() 会自动追踪回调函数在同步执行时访问的所有响应式数据。
 但有个重要提示:
如果回调是异步函数,那么只有在第一个
await之前访问的数据才会被追踪!
watchEffect(async () => {console.log(todoId.value) // ✅ 被追踪,因为在 await 之前访问const response = await fetch('...')console.log(someOtherRef.value) // ❌ 不被追踪,因为在 await 之后访问
})
 
原因是:
-  
Vue 只能追踪同步执行阶段访问的数据。
 -  
在异步操作完成后的回调内访问的数据不会被 Vue 追踪。
 
3、watchEffect() 在实际场景中的优势:
 
优势一:自动跟踪多个数据源(不必手动指定):
假如你有多个响应式数据:
const firstName = ref('Tom')
const lastName = ref('Jerry')watchEffect(() => {console.log(`Name: ${firstName.value} ${lastName.value}`)
})Vue 自动监控 firstName 和 lastName。无论哪个改变,都会触发回调函数。使用 watch() 则必须手动指定数据源:
watch([firstName, lastName], () => {console.log(`Name: ${firstName.value} ${lastName.value}`)
})
 
优势二:更精细地跟踪对象属性(比深监听高效):
假如你有复杂对象:
const user = ref({name: 'Tom',age: 20,address: { city: 'Shanghai', street: 'Main St' }
})watchEffect(() => {console.log(`User city: ${user.value.address.city}`)
})watchEffect() 只监听了对象的部分属性 (address.city),高效、精准。如果用深监听 (watch(user, ..., { deep: true })),会监听所有属性的变化,性能可能较差。
 
4、什么时候用 watch()?什么时候用 watchEffect()?
 
| 场景 | 推荐方式 | 理由 | 
|---|---|---|
| 明确知道监听的数据源 | ✅ 使用 watch() | 明确指定,粒度精准 | 
| 多个数据源或依赖复杂 | ✅ 使用 watchEffect() | 自动跟踪,代码更简洁、更灵活 | 
| 动态数据请求或复杂副作用 | ✅ watchEffect() | 自动监听,省去手动指定烦恼 | 
4、监听属性的常见使用场景
| 场景 | 示例 | 
|---|---|
| 数据变化请求API | 表单值变化时重新获取数据 | 
| 数据变化存储数据 | 自动保存用户输入 | 
| 执行副作用 | 数据变化时更新DOM或执行动画 | 
5、监听属性的注意事项
-  
避免无限循环:
watch(count, (val) => {count.value++ // ⚠️ 无限循环,不要这样做 }) -  
不要监听非响应式数据(监听无效):
const plain = { name: 'Tom' } watch(plain, () => {}) // ❌ 无效 -  
使用深监听时注意性能问题(深监听成本较高):
watch(obj, () => {}, { deep: true }) // 谨慎使用注意,你不能直接侦听响应式对象的属性值,例如、
 
const obj = reactive({ count: 0 })// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {console.log(`Count is: ${count}`)
}) 
这里需要用一个返回该属性的 getter 函数:
// 提供一个 getter 函数
watch(() => obj.count,(count) => {console.log(`Count is: ${count}`)}
) 
6、副作用清理
在 Vue 中,所谓的副作用通常指:
-  
异步请求(如 API 请求)
 -  
定时器 (
setTimeout、setInterval) -  
DOM 操作、监听事件
 -  
其他非纯函数的逻辑
 
这些操作不是立即完成的,可能会在未来某个时刻继续执行。
为什么需要副作用清理?
以 API 请求为例:假设我们有一个监听器监听 id:
watch(id, (newId) => {fetch(`/api/${newId}`).then(() => {console.log('请求完成,当前ID:', newId)})
}) 
可能的问题:
当你快速修改 id:
-  
请求 1 (
/api/1) 发出后,还未完成。 -  
请求 2 (
/api/2) 立即发出。 -  
如果请求 2 的响应比请求 1 快,那么请求 1 的响应(较慢)回来时,结果是过时的,但还是会被处理。
 
我们想要的:
-  
当数据变化时,上一个异步请求应被取消或忽略,不再执行后续逻辑。
 
为了解决这个问题,Vue 提供了副作用清理机制。
副作用清理函数 (onCleanup())
 
从 Vue 3.0 开始,Vue 提供了一个清理机制,称为 onCleanup。
watch(id, (newId, oldId, onCleanup) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then((res) => res.json()).then((data) => {console.log('请求结果:', data)})onCleanup(() => {controller.abort() // 取消上一个请求})
})
 
含义解释:
-  
每次监听的数据 (
id) 变化时:-  
先调用上一次注册的
onCleanup清理函数。 -  
然后再执行新一次监听回调。
 
 -  
 -  
因此,上一次的异步请求会自动终止,避免过时请求的结果被错误处理。
 
 watchEffect() 中的副作用清理
watchEffect((onCleanup) => {const timer = setInterval(() => {console.log('定时执行')}, 1000)onCleanup(() => {clearInterval(timer) // 清理定时器})
})
 
原理相同:
-  
每次响应式数据变化重新执行副作用之前,先调用清理函数。
 -  
确保副作用(定时器、请求等)不重叠,避免内存泄漏或数据错乱。
 
从 Vue 3.5 版本开始,引入了新 API:onWatcherCleanup(): 
import { watch, onWatcherCleanup } from 'vue'watch(id, (newId) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {console.log('请求完成:', newId)})onWatcherCleanup(() => {controller.abort() // 取消上一个请求})
})
 
特点:
-  
不再需要第三个参数
onCleanup。 -  
可以独立地在监听器或
watchEffect()回调函数内调用清理函数。 
使用限制:
-  
必须在同步阶段调用,不可放在
await之后。 -  
因此,必须在异步操作之前注册。
 
正确用法(同步调用):watch(id, (newId) => {const controller = new AbortController()onWatcherCleanup(() => controller.abort()) // 同步调用,正确!fetch(`/api/${newId}`, { signal: controller.signal })
})❌ 错误用法(异步调用):watch(id, async (newId) => {const controller = new AbortController()await someAsyncOperation()onWatcherCleanup(() => controller.abort()) // ❌ 错误!异步调用