跳到主要内容

computed vs watch

问题

Vue 中 computed 和 watch 有什么区别?它们各自的使用场景是什么?

答案

computedwatch 都是响应式 API,但设计目的不同:

特性computedwatch / 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

特性watchEffectwatch
依赖追踪自动显式声明
立即执行默认是需要 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有缓存,自动依赖追踪
需要执行异步操作watchcomputed 不支持异步
需要访问旧值watchcomputed 没有旧值
需要执行 DOM 操作watchcomputed 不应有副作用
数据变化后发请求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);
});
维度watchwatchEffect
依赖声明显式自动推断
执行时机依赖变化时立即执行 + 依赖变化时
旧值访问(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;
}
};
}
  1. 首次访问dirty=true,执行 getter,缓存结果,dirty=false
  2. 再次访问dirty=false,直接返回缓存
  3. 依赖变化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 });

原因:

  1. computed 需要同步返回值供模板使用
  2. computed 的缓存机制基于同步依赖追踪
  3. 异步操作属于副作用,应该用 watch

Q5: watchEffect 和 watch 的区别?什么时候用 watchEffect?

答案

watchEffectwatch 都用于响应式地执行副作用,但在依赖追踪方式、执行时机和 API 形式上有本质区别:

维度watchwatchEffect
依赖声明显式指定侦听源自动追踪回调中使用的所有响应式数据
首次执行默认不执行(需 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),等到首次被访问
首次访问 .valuetruefalse执行 getter,缓存结果到 _value
再次访问 .valuefalse直接返回 _value,不重新计算
依赖变化falsetrue通过 scheduler 标记为脏,但仍不计算
依赖变化后再访问truefalse重新执行 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

在模板中,computedmethods 调用都能得到相同结果,但 computed 有缓存——多次访问只计算一次。如果计算开销大(如遍历大数组),computed 的缓存优势更明显。

相关链接