在 项目开发中,我们经常需要处理数据的动态变化,例如:
methods
中调用,每次都会重复执行,导致性能下降。在这些场景下,Vue 提供了 Computed(计算属性) 和 Watch(侦听器) 作为响应式解决方案。
然而,很多我们使用时会有这样的问题:
computed
不会重复计算,而 watch
却会反复触发?computed
和 watch
看似都能监听数据,实际场景该如何选择?watch
为什么有 deep
选项,而 computed
没有?本文通过源码解析、依赖收集机制、调度执行、性能优化及最佳实践等多个角度,深入探讨 computed
和 watch
的区别,并结合项目实际案例,理解和应用 Vue 的响应式特性。
在 Vue 2 的源码中,computed
是通过 Watcher
实现的,关键逻辑如下:
computedWatcher
实例:当 computed
被定义时,Vue 内部会创建一个 Watcher
实例,并将 lazy
设置为 true
(惰性计算)。computed
依赖的数据(响应式数据)变化时,该 computedWatcher
会被标记为 dirty
,但不会立即重新计算。computed
计算属性被访问时,Vue 发现 dirty = true
,才会执行计算并缓存结果。dirty = false
,则直接返回上次计算的结果,而不会重复计算。在 Vue 2 的 src/core/instance/state.js
文件中,computed
的初始化主要依赖 defineComputed()
函数:
function defineComputed(target, key, userDef) {
const getter = typeof userDef === 'function' ? userDef : userDef.get;
sharedPropertyDefinition.get = function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate(); // 计算新的值并缓存
}
if (Dep.target) {
watcher.depend(); // 进行依赖收集
}
return watcher.value;
}
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
computed
依赖 Watcher
进行依赖收集,但 Watcher
只有在访问 computed
时才会执行计算(惰性计算)。watcher.dirty
作为缓存标志,只有在依赖变化时才会重新计算。computed
具有缓存特性,即使被多次访问,只要依赖未变化,都不会重新计算,从而提升性能。watch
,computed
仅在依赖变更时更新,避免了 watch
可能引起的额外副作用执行。
计算属性:{{ reversedMessage }}
方法调用:{{ reverseMessage() }}
computed
只会执行一次,后续访问都返回缓存值。methods
每次调用都会重新计算,浪费性能。watch
在 Vue 2 中同样依赖 Watcher
进行依赖追踪,核心机制包括:
Watcher
实例:watch
监听的数据变化时,Vue 触发 Watcher
实例执行回调函数。watch
在组件更新之后执行(nextTick
队列)。watch
默认是浅监听,如果监听的是对象或数组,需要设置 deep: true
进行递归监听。watch
的实现)在 Vue 2 的 src/core/observer/watcher.js
文件中,watch
的核心实现如下:
class Watcher {
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn);
this.value = this.get();
}
get() {
Dep.target = this; // 依赖收集
let value = this.getter.call(this.vm, this.vm);
Dep.target = null;
return value;
}
update() {
const newVal = this.get();
const oldVal = this.value;
this.value = newVal;
this.cb.call(this.vm, newVal, oldVal);
}
}
Dep.target = this
用于收集依赖,触发回调。cb.call(this.vm, newVal, oldVal)
当数据变化时执行用户提供的回调函数。watch(
() => user,
(newVal, oldVal) => console.log("用户数据变化", newVal),
{ deep: true }
);
如果 user
对象非常庞大,Vue 需要递归遍历所有属性,会增加性能开销。
特性 |
Computed(计算属性) |
Watch(侦听器) |
缓存 |
是(有缓存) |
否(无缓存) |
执行时机 |
访问时触发计算 |
依赖数据变化后立即执行 |
适用场景 |
计算派生数据 |
监听数据并执行副作用 |
支持异步 |
否 |
是 |
深度监听 |
否 |
是(deep: true) |
在许多实际场景中,我们可以结合 computed
和 watch
进行优化:
const fullName = computed(() => `${user.firstName} ${user.lastName}`);
watch(fullName, (newVal) => {
console.log("用户姓名变化:", newVal);
});
computed
适用于计算派生数据,具备缓存特性,提升性能。watch
适用于监听数据变化并执行副作用(如 API 请求、手动 DOM 操作)。computed
应优先使用,只有在 computed
不能满足需求时才使用 watch
。watch
监听整个对象,结合 computed
提高效率。