在 Vue 3 中,computed
和 watch
(以及 watchEffect
)是处理响应式数据的核心工具,它们都能响应数据变化,但设计目的、使用场景和行为特性有显著区别。
一、computed:计算属性(依赖缓存 + 懒执行)
1.1 定义与用途
computed
用于创建基于其他响应式数据派生出的新数据。它是一个"智能工厂",根据依赖的响应式数据自动计算并返回一个值。
- 核心特点 :自动追踪依赖、具备缓存机制、值是响应式的。
- 适用场景 :
- 根据现有数据计算新值(如总价、反转字符串)。
- 简化模板中的复杂逻辑。
- 需要缓存结果以提高性能的计算。
1.2 行为特性
- 缓存性 :只要依赖的数据未变,多次访问
computed
属性会直接返回缓存结果,不会重新执行计算函数。 - 惰性求值:只有在被访问时才会执行计算(首次访问或依赖变化后)。
- 同步执行:计算过程必须是同步的。
1.3 使用示例
import { ref, computed } from 'vue';
const price = ref(10);
const quantity = ref(5);
// 计算总价
const totalPrice = computed(() => {
console.log('计算中...'); // 仅当 price 或 quantity 变化时才执行
return price.value * quantity.value;
});
console.log(totalPrice.value); // 50 (执行计算)
console.log(totalPrice.value); // 50 (直接返回缓存,不打印日志)
price.value = 15;
console.log(totalPrice.value); // 75 (依赖变化,重新计算)
1.4 可写计算属性
const number = ref(4);
const squaredNumber = computed({
get: () => number.value * number.value,
set: (newValue) => {
number.value = Math.sqrt(newValue);
}
});
squaredNumber.value = 25; // 触发 set,number.value 变为 5
二、watch:显式监听(精确控制 + 支持异步)
2.1 定义与用途
watch
用于监听特定数据的变化,并在变化时执行副作用操作(如异步请求、日志打印、复杂逻辑)。
- 核心特点:明确指定监听源、执行副作用、可配置选项丰富。
- 适用场景 :
- 数据变化时需要执行异步操作(如 API 请求)。
- 需要获取新值和旧值进行比较。
- 监听深层对象或数组的变化。
2.2 行为特性
- 无缓存:每次数据变化都会执行回调函数。
- 可配置 :
immediate: true
:创建时立即执行一次回调。deep: true
:深度监听对象/数组内部变化。
- 返回停止函数:调用返回的函数可停止侦听。
2.3 使用示例
import { ref, watch } from 'vue';
const searchQuery = ref('');
// 监听 searchQuery 变化,执行搜索
const stopWatch = watch(searchQuery, (newVal, oldVal) => {
console.log(`搜索从 "${oldVal}" 变为 "${newVal}"`);
if (newVal) {
// 模拟异步搜索
fetch(`/api/search?q=${newVal}`)
.then(response => response.json())
.then(data => updateResults(data));
}
}, {
immediate: false, // 是否立即执行
deep: false // 是否深度监听
});
// 停止监听
// stopWatch();
2.4 监听多个源
const firstName = ref('');
const lastName = ref('');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('姓名变化:', newFirst, newLast);
});

2.5 监听 ref
包裹的复杂对象
import { ref, watch } from 'vue'
const state = ref({
list: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
})
watch(
() => state.value,
(newVal, oldVal) => {
console.log('list changed')
},
{ deep: true }
)
// 修改数组内部对象
state.value.list[0].name = 'Updated Item' // 触发
2.6:监听 ref
的值(数组或对象)
const books = ref(['Vue Guide', 'React Guide'])
watch(
books,
(newBooks, oldBooks) => {
console.log('books changed')
},
{ deep: true }
)
books.value.push('TypeScript Guide') // 触发
💡 注意:对于
ref
,你可以直接传入ref
本身作为监听源,Vue 会自动解包。

2.7 监听 reactive 对象
import { reactive } from 'vue';
const state = reactive({
name: 'Alice',
age: 25
});
// 监听整个 reactive 对象(需 deep: true)
watch(state, (newVal, oldVal) => {
console.log('state 全局变化:', newVal);
}, { deep: true });
// 监听某个具体属性
watch(() => state.name, (newName) => {
console.log('姓名变了:', newName);
});
⚠️ 注意:直接
watch(state, ...)
不加deep: true
,只能监听state
引用变化(一般不会变),无法监听内部属性变化。
⚠️ 注意事项
-
性能开销
deep: true
会递归遍历对象的所有嵌套属性,建立监听依赖,可能带来性能开销,尤其是在监听大型对象时。 -
oldValue 是代理对象
使用
deep: true
时,oldVal
是响应式代理(Proxy),修改它会影响原始数据:watch( () => user, (newVal, oldVal) => { oldVal.name = 'hacked' // ❌ 不要修改 oldVal! }, { deep: true } )
-
默认行为
- 对于
reactive
对象:监听其引用变化,但不会自动深度监听内部嵌套属性(除非你访问了它们)。 - 对于
ref
:监听.value
的变化。
- 对于
🆚 与 watchEffect
的对比
特性 | watch + deep: true |
watchEffect |
---|---|---|
是否需要指定监听源 | ✅ 需要 | ❌ 不需要(自动追踪) |
深度监听方式 | 强制递归监听整个对象树 | 只监听实际访问过的属性 |
性能 | 可能较慢(深度遍历) | 更高效(精确依赖追踪) |
适用场景 | 需要监听整个对象所有变化 | 自动追踪副作用中用到的响应式数据 |
✅ 最佳实践
- ✅ 需要监听整个对象/数组的所有变化 → 使用
watch
+deep: true
- ✅ 只关心某些特定属性 → 使用
watch
监听 getter 函数,避免deep
- ✅ 副作用逻辑复杂,依赖不明确 → 使用
watchEffect
(自动追踪)
示例:避免不必要的 deep
// 更高效的方式:只监听特定字段
watch(
() => user.profile.age,
(newAge) => {
console.log('Age changed to:', newAge)
}
)
// 不需要 deep: true
总结
deep: true
用于强制watch
监听对象或数组的所有嵌套属性变化。- 适用于需要监听复杂结构整体变化的场景。
- 注意性能影响,优先考虑精确监听 或使用
watchEffect
自动追踪依赖。
三、watchEffect:自动监听(副作用优先 + 简化代码)
3.1 定义与用途
watchEffect
会立即执行传入的函数,并自动追踪其内部访问的所有响应式数据作为依赖。当任何依赖变化时,函数会重新执行。
- 核心特点:自动依赖收集、立即执行、简化代码。
- 适用场景 :
- 副作用逻辑简单且依赖关系明确。
- 不想手动指定监听源。
3.2 使用示例
import { ref, watchEffect } from 'vue';
const count = ref(0);
const enabled = ref(true);
watchEffect(() => {
if (enabled.value) {
console.log('Count is:', count.value); // 自动监听 count 和 enabled
}
});
count.value++; // 输出: Count is: 1
enabled.value = false;
count.value++; // 无输出(enabled 为 false)
enabled.value = true; // 输出: Count is: 2(重新执行)
四、在组件中如何安全地取消监听(结合 onUnmounted
)
在 Vue 3 中,无论是 watch
还是 watchEffect
,调用它们都会返回一个停止函数(stop function)。调用这个函数即可停止侦听,防止不必要的性能开销或内存泄漏(尤其是在组件卸载前未清理的侦听器)。
在 Vue 组件中,通常建议在组件卸载时自动停止侦听,避免内存泄漏。
💡 最佳实践 :如果你在
setup()
或script setup
中使用watch
/watchEffect
,并且侦听的是组件内部状态,通常 Vue 会在组件销毁时自动清理。但如果你侦听的是全局状态、外部响应式对象,或想提前控制,手动调用stop()
是推荐做法。
什么情况下"必须"清理?
场景 | 是否必须清理 | 说明 |
---|---|---|
监听 window , document 事件 |
✅ 必须 | 全局对象,不会自动销毁 |
使用 watchEffect 读取全局变量 |
✅ 建议清理 | 防止副作用继续执行 |
监听 Pinia 全局 store | ⚠️ 通常不需要 | Pinia 会在组件销毁时自动清理 |
监听组件内 ref /reactive |
❌ 不需要 | Vue 自动管理生命周期 |
WebSocket / EventSource | ✅ 必须 | 手动关闭连接,防止内存泄漏 |
组件卸载时必须调用 stop()
清理的案例
<!-- ResponsivePanel.vue -->
<script setup>
import { ref, watchEffect, onUnmounted } from 'vue';
const width = ref(window.innerWidth);
const isMobile = ref(width.value < 768);
// 🔴 关键:监听全局 window 事件
const stop = watchEffect(() => {
// watchEffect 自动追踪 window.innerWidth
width.value = window.innerWidth;
isMobile.value = width.value < 768;
console.log(`窗口宽度更新: ${width.value}px, 移动端: ${isMobile.value}`);
// 假设这里还触发了某些 UI 逻辑或 API 请求
});
// ✅ 必须在组件卸载时清理!否则内存泄漏
onUnmounted(() => {
stop();
console.log('✅ window resize 侦听器已清理');
});
</script>
<template>
<div class="panel" :class="{ mobile: isMobile, desktop: !isMobile }">
<h3>响应式面板</h3>
<p>当前宽度: {{ width }}px</p>
<p>设备类型: {{ isMobile ? '移动端' : '桌面端' }}</p>
</div>
</template>
<style>
.panel.mobile { background: #ffe4e1; padding: 20px; }
.panel.desktop { background: #e1f5fe; padding: 40px; }
</style>
❌ 如果不清理会发生什么?
假设用户:
- 打开
ResponsivePanel
组件(监听开始) - 切换到其他页面,但组件未正确清理
watchEffect
- 继续调整窗口大小
👉 后果:
watchEffect
回调仍会执行(因为window
事件还在触发)console.log
依然打印- 如果里面有
fetch
、emit
、store
操作,会导致:- 重复请求 API
- 更新已销毁组件的状态
- 内存泄漏(监听器无法被垃圾回收)
WebSocket 全局连接(必须清理)
<!-- LiveChat.vue -->
<script setup>
import { ref, watch, onUnmounted } from 'vue';
const messages = ref([]);
const isConnected = ref(false);
let socket = null;
// 启动 WebSocket 连接
function connect() {
socket = new WebSocket('wss://example.com/chat');
socket.onopen = () => {
isConnected.value = true;
};
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
messages.value.push(msg);
};
}
connect();
// 🔴 监听全局 WebSocket 消息(虽然是在 setup 中,但 socket 是全局资源)
const stopWatch = watch(
() => messages.value.length,
(newCount) => {
console.log(`收到新消息,总数: ${newCount}`);
// 更新通知、播放声音等
}
);
// ✅ 必须清理:关闭连接 + 停止侦听
onUnmounted(() => {
if (socket) {
socket.close();
console.log('WebSocket 已关闭');
}
stopWatch(); // 停止 watch
console.log('✅ LiveChat 侦听器已清理');
});
</script>
⚠️ 如果不调用
socket.close()
和stopWatch()
:
- WebSocket 连接可能保持打开,持续接收消息
watch
回调不断执行,试图更新已销毁的组件- 严重内存泄漏和性能问题
📌 这就是必须手动清理的原因 :window
、document
、WebSocket
、EventSource
等全局对象的事件不会随组件销毁而自动断开。
者对比总结
特性 | computed |
watch |
watchEffect |
---|---|---|---|
主要用途 | 派生新数据 | 执行副作用(异步/复杂逻辑) | 执行副作用(自动依赖) |
返回值 | 有(响应式引用) | 无(返回停止函数) | 无(返回停止函数) |
缓存 | ✅ 有缓存 | ❌ 无缓存 | ❌ 无缓存 |
依赖追踪 | 自动 | 手动指定 | 自动 |
初始执行 | 访问时执行 | 可配置 immediate |
立即执行 |
适用异步 | ❌ 同步 | ✅ 可异步 | ✅ 可异步 |
五、如何选择?
-
用
computed
:- 需要根据数据计算出一个值,并在模板或多处使用。
- 计算可能较复杂,需要缓存提升性能。
- 例如:总价、过滤后的列表、反转字符串。
-
用
watch
:- 需要在数据变化时执行异步操作(如 API 调用)。
- 需要精确控制监听源或使用
deep
/immediate
选项。 - 需要比较新旧值。
- 例如:搜索框防抖请求、表单验证、数据持久化。
-
用
watchEffect
:- 副作用逻辑简单,且希望自动追踪依赖。
- 不想手动管理依赖列表。
- 例如:同步状态到外部系统、简单的日志记录。
六、面试高频问题
Q1: computed
和 watch
的主要区别?
computed
用于计算并返回一个新值 ,具有缓存,适合同步计算;watch
用于监听变化并执行副作用,适合异步或复杂逻辑。
Q2: 为什么 computed
有缓存而 watch
没有?
computed
的本质是"属性",其值应与依赖一一对应,缓存避免重复计算提升性能。watch
是"监听器",每次变化都应触发回调以执行必要的操作。
Q3: watch
和 watchEffect
如何选择?
当需要明确指定监听源或使用
immediate
/deep
时用watch
;当希望自动追踪依赖且逻辑简单时用watchEffect
。
正确理解并选择 computed
、watch
和 watchEffect
,是编写高效、可维护 Vue 3 应用的关键。