computed vs watch
问题
Vue 中 computed 和 watch 有什么区别?它们各自的使用场景是什么?
答案
computed 和 watch 都是响应式 API,但设计目的不同:
| 特性 | computed | watch / watchEffect |
|---|---|---|
| 用途 | 派生数据 | 响应副作用 |
| 返回值 | 有(计算结果) | 无 |
| 缓存 | ✅ 依赖不变不重新计算 | ❌ 每次都执行 |
| 副作用 | 应避免 | 专门用于副作用 |
| 同步/异步 | 同步 | 可以异步 |
computed - 计算属性
computed 用于派生状态,根据已有数据计算出新数据:
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读计算属性
const fullName = computed(() => {
console.log('computed 被调用'); // 依赖变化才会打印
return `${firstName.value} ${lastName.value}`;
});
console.log(fullName.value); // "John Doe"
console.log(fullName.value); // "John Doe"(缓存,不会重新计算)
firstName.value = 'Jane';
console.log(fullName.value); // "Jane Doe"(依赖变化,重新计算)
可写计算属性
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue: string) {
const [first, last] = newValue.split(' ');
firstName.value = first;
lastName.value = last;
}
});
fullName.value = 'Bob Smith'; // 触发 setter
console.log(firstName.value); // "Bob"
console.log(lastName.value); // "Smith"
computed 的缓存特性
const now = computed(() => Date.now());
console.log(now.value); // 1708329600000
console.log(now.value); // 1708329600000(相同值,因为没有响应式依赖)
// 如果需要实时时间,应该用 ref + setInterval
watch - 侦听器
watch 用于侦听数据变化并执行副作用:
import { ref, watch } from 'vue';
const count = ref(0);
// 基础用法
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
});
// 侦听多个源
const name = ref('Tom');
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} → ${newCount}`);
console.log(`name: ${oldName} → ${newName}`);
});
// 侦听 getter 函数
const obj = ref({ count: 0 });
watch(
() => obj.value.count,
(newVal) => console.log(`obj.count: ${newVal}`)
);
watch 配置选项
watch(source, callback, {
immediate: true, // 立即执行一次
deep: true, // 深度侦听
flush: 'post', // 回调时机:'pre' | 'post' | 'sync'
once: true, // 只触发一次(Vue 3.4+)
});
// immediate:立即执行
const data = ref(null);
watch(data, (val) => {
console.log('data:', val);
}, { immediate: true });
// 立即输出: "data: null"
// deep:深度侦听
const user = ref({ profile: { name: 'Tom' } });
watch(user, (val) => {
console.log('user changed');
}, { deep: true }); // 修改 user.value.profile.name 也会触发
// flush: 'post':在 DOM 更新后执行
watch(count, () => {
console.log(document.querySelector('#count').textContent);
}, { flush: 'post' });
watchEffect - 自动追踪依赖
watchEffect 自动追踪回调中使用的响应式数据:
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('Tom');
// 自动追踪 count 和 name
const stop = watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`);
});
// 立即执行一次,输出: "count: 0, name: Tom"
count.value++; // 输出: "count: 1, name: Tom"
name.value = 'Jerry'; // 输出: "count: 1, name: Jerry"
// 停止侦听
stop();
watchEffect vs watch
| 特性 | watchEffect | watch |
|---|---|---|
| 依赖追踪 | 自动 | 显式声明 |
| 立即执行 | 默认是 | 需要 immediate: true |
| 获取旧值 | ❌ | ✅ |
| 侦听特定源 | ❌ | ✅ |
// watchEffect:适合复杂依赖
watchEffect(() => {
// 自动追踪所有使用的响应式数据
if (userLoggedIn.value) {
fetchUserProfile(userId.value);
}
});
// watch:适合精确控制
watch(userId, async (newId, oldId) => {
// 只在 userId 变化时执行
// 可以获取新旧值
await fetchUserProfile(newId);
});
清理副作用
import { watchEffect, watch } from 'vue';
// watchEffect 清理
watchEffect((onCleanup) => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => console.log(data));
// 在下次执行前或组件卸载时调用
onCleanup(() => {
controller.abort();
});
});
// watch 清理
watch(searchQuery, async (query, oldQuery, onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
const data = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
});
watchPostEffect 和 watchSyncEffect
import { watchPostEffect, watchSyncEffect } from 'vue';
// watchPostEffect = watchEffect + { flush: 'post' }
// 在 DOM 更新后执行
watchPostEffect(() => {
console.log(document.getElementById('count')?.textContent);
});
// watchSyncEffect = watchEffect + { flush: 'sync' }
// 同步执行,性能敏感场景慎用
watchSyncEffect(() => {
console.log('同步执行');
});
computed 实现原理
// 简化版 computed 实现
function computed<T>(getter: () => T) {
let value: T;
let dirty = true; // 是否需要重新计算
const effectFn = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true;
// 触发依赖此 computed 的 effect
trigger(obj, 'value');
}
}
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
// 收集依赖此 computed 的 effect
track(obj, 'value');
return value;
}
};
return obj;
}
computed 的懒执行
computed 只在被访问时才会计算值,如果没有被使用,即使依赖变化也不会计算。
常见面试问题
Q1: computed 和 watch 该如何选择?
答案:
| 场景 | 选择 | 原因 |
|---|---|---|
| 从已有数据派生新数据 | computed | 有缓存,自动依赖追踪 |
| 需要执行异步操作 | watch | computed 不支持异步 |
| 需要访问旧值 | watch | computed 没有旧值 |
| 需要执行 DOM 操作 | watch | computed 不应有副作用 |
| 数据变化后发请求 | watch | 典型的副作用场景 |
// ✅ computed:派生数据
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// ✅ watch:副作用
watch(userId, async (id) => {
userData.value = await fetchUser(id);
});
// ❌ 错误:computed 执行副作用
const data = computed(() => {
fetch('/api/data'); // 不要这样做!
return someValue.value;
});
Q2: watch 和 watchEffect 的区别?
答案:
const count = ref(0);
const name = ref('Tom');
// watch:显式声明依赖
watch(count, (newVal, oldVal) => {
console.log('count changed:', oldVal, '→', newVal);
});
// watchEffect:自动追踪
watchEffect(() => {
// 自动追踪 count 和 name
console.log(count.value, name.value);
});
| 维度 | watch | watchEffect |
|---|---|---|
| 依赖声明 | 显式 | 自动推断 |
| 执行时机 | 依赖变化时 | 立即执行 + 依赖变化时 |
| 旧值访问 | ✅ (newVal, oldVal) | ❌ |
| 精确控制 | ✅ 只侦听指定源 | ❌ 追踪所有使用的响应式数据 |
选择建议:
- 需要旧值或精确控制 →
watch - 依赖复杂或需要立即执行 →
watchEffect
Q3: computed 的缓存是如何实现的?
答案:
computed 使用 dirty 标志位实现缓存:
function computed(getter) {
let cachedValue;
let dirty = true; // 脏标志
const effect = new ReactiveEffect(getter, () => {
// 依赖变化时,标记为脏,但不立即计算
dirty = true;
});
return {
get value() {
if (dirty) {
// 脏时才重新计算
cachedValue = effect.run();
dirty = false;
}
return cachedValue;
}
};
}
- 首次访问:
dirty=true,执行 getter,缓存结果,dirty=false - 再次访问:
dirty=false,直接返回缓存 - 依赖变化:
dirty=true,下次访问时重新计算
Q4: 为什么 computed 不能执行异步操作?
答案:
computed 是同步的,必须立即返回值:
// ❌ 错误:computed 不支持异步
const asyncData = computed(async () => {
const data = await fetch('/api/data');
return data.json(); // 返回的是 Promise,不是数据
});
console.log(asyncData.value); // Promise { <pending> }
// ✅ 正确:使用 watch 或 watchEffect
const data = ref(null);
watch(source, async () => {
data.value = await fetch('/api/data').then(r => r.json());
}, { immediate: true });
原因:
- computed 需要同步返回值供模板使用
- computed 的缓存机制基于同步依赖追踪
- 异步操作属于副作用,应该用 watch
Q5: watchEffect 和 watch 的区别?什么时候用 watchEffect?
答案:
watchEffect 和 watch 都用于响应式地执行副作用,但在依赖追踪方式、执行时机和 API 形式上有本质区别:
| 维度 | watch | watchEffect |
|---|---|---|
| 依赖声明 | 显式指定侦听源 | 自动追踪回调中使用的所有响应式数据 |
| 首次执行 | 默认不执行(需 immediate: true) | 立即执行一次 |
| 新旧值 | ✅ (newVal, oldVal) | ❌ 无法获取 |
| 侦听精度 | 精确控制侦听哪些数据 | 追踪回调中所有被读取的响应式数据 |
| 停止侦听 | 返回 stop 函数 | 返回 stop 函数 |
| flush 选项 | 支持 'pre' | 'post' | 'sync' | 同样支持,且有 watchPostEffect / watchSyncEffect 快捷方式 |
import { ref, watch, watchEffect } from 'vue';
const userId = ref(1);
const userInfo = ref<{ name: string } | null>(null);
const isLoggedIn = ref(true);
// ✅ watch:精确侦听 userId,可拿到新旧值
watch(userId, async (newId, oldId) => {
console.log(`userId 从 ${oldId} 变为 ${newId}`);
userInfo.value = await fetchUser(newId);
});
// ✅ watchEffect:自动追踪 isLoggedIn 和 userId
// 任何一个变化都会重新执行
watchEffect(async () => {
if (isLoggedIn.value) {
userInfo.value = await fetchUser(userId.value);
}
});
什么时候用 watchEffect?
- 依赖项多且复杂:回调中用到很多响应式变量,逐一声明 watch source 太繁琐
- 需要立即执行:
watchEffect默认立即执行,省去immediate: true - 不需要旧值:只关心"当前状态是什么",不关心"从什么变成什么"
- 组合多个响应式数据的副作用:自动追踪能减少遗漏
反之,如果你需要旧值对比、精确控制侦听源或条件性地决定是否执行,用 watch 更合适。
一个常见的最佳实践是在组件初始化时使用 watchEffect 做数据请求:
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
const searchKeyword = ref('');
const category = ref('all');
const results = ref<string[]>([]);
// 搜索关键词或分类变化时,自动重新搜索
// 且组件挂载时立即执行一次
watchEffect(async (onCleanup) => {
const controller = new AbortController();
onCleanup(() => controller.abort());
const res = await fetch(
`/api/search?q=${searchKeyword.value}&cat=${category.value}`,
{ signal: controller.signal }
);
results.value = await res.json();
});
</script>
Q6: computed 的缓存机制是怎么实现的?(dirty flag + lazy evaluation)
答案:
Vue 3 的 computed 通过 dirty flag(脏标志) 和 lazy evaluation(延迟求值) 两个核心机制实现缓存。下面从原理到源码层面进行解析。
核心流程
详细源码解析
// 简化版 Vue 3 computed 实现
class ComputedRefImpl<T> {
private _value!: T;
private _dirty = true; // 脏标志:是否需要重新计算
public readonly effect: ReactiveEffect<T>;
public readonly __v_isRef = true;
constructor(getter: () => T) {
// 创建 ReactiveEffect,但设置 lazy: true,不立即执行
this.effect = new ReactiveEffect(getter, () => {
// scheduler:依赖变化时的回调
if (!this._dirty) {
this._dirty = true; // 1. 标记为脏
triggerRefValue(this); // 2. 通知依赖此 computed 的副作用
}
});
this.effect.computed = this;
}
get value() {
trackRefValue(this); // 收集依赖此 computed 的 effect
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run()!; // 执行 getter,缓存结果
}
return this._value;
}
}
三个阶段详解
| 阶段 | dirty 状态 | 行为 |
|---|---|---|
| 创建时 | true | 不立即计算(lazy evaluation),等到首次被访问 |
首次访问 .value | true → false | 执行 getter,缓存结果到 _value |
再次访问 .value | false | 直接返回 _value,不重新计算 |
| 依赖变化 | false → true | 通过 scheduler 标记为脏,但仍不计算 |
| 依赖变化后再访问 | true → false | 重新执行 getter,更新缓存 |
为什么不在依赖变化时立即重新计算?
这就是 lazy evaluation 的核心:如果 computed 的值没有被任何地方使用(没有访问 .value),即使依赖变化了也不浪费算力。只有在真正被读取时才计算,最大化性能。
验证缓存行为
import { ref, computed, effect } from 'vue';
const price = ref(100);
const quantity = ref(2);
let computeCount = 0;
const total = computed(() => {
computeCount++;
console.log('computed 执行了');
return price.value * quantity.value;
});
// 1. 未访问 .value,computed 不会执行
console.log(computeCount); // 0(lazy,未执行)
// 2. 首次访问
console.log(total.value); // "computed 执行了",200
console.log(computeCount); // 1
// 3. 再次访问(缓存命中)
console.log(total.value); // 200(没有打印 "computed 执行了")
console.log(computeCount); // 1(没有增加)
// 4. 修改依赖
price.value = 200;
console.log(computeCount); // 1(dirty=true,但还没重新计算)
// 5. 再次访问(缓存失效,重新计算)
console.log(total.value); // "computed 执行了",400
console.log(computeCount); // 2
computed 的链式依赖
computed 还支持 computed 依赖 computed 的链式场景:
const price = ref(100);
const quantity = ref(2);
const subtotal = computed(() => price.value * quantity.value); // 依赖 price, quantity
const tax = computed(() => subtotal.value * 0.1); // 依赖 subtotal
const total = computed(() => subtotal.value + tax.value); // 依赖 subtotal, tax
console.log(total.value); // 220
price.value = 200;
// subtotal 的 dirty → true → tax 的 dirty → true → total 的 dirty → true
// 访问 total.value 时沿链路依次重新计算
console.log(total.value); // 440
面试延伸:computed vs method
在模板中,computed 和 methods 调用都能得到相同结果,但 computed 有缓存——多次访问只计算一次。如果计算开销大(如遍历大数组),computed 的缓存优势更明显。