1. 概述
在 项目开发中,我们经常需要处理数据的动态变化,例如:
- 性能问题 :某个数据需要经过复杂计算,直接在
methods中调用,每次都会重复执行,导致性能下降。 - 副作用控制 :想要在数据变化时执行异步请求,但又发现某些不必要的调用导致接口请求过多,影响用户体验。
- 数据依赖管理:某些变量需要依赖其他多个数据源,但逻辑复杂,不容易维护。
在这些场景下,Vue 提供了 Computed(计算属性) 和 Watch(侦听器) 作为响应式解决方案。
然而,很多我们使用时会有这样的问题:
- 为什么
computed不会重复计算,而watch却会反复触发? computed和watch看似都能监听数据,实际场景该如何选择?watch为什么有deep选项,而computed没有?
本文通过源码解析、依赖收集机制、调度执行、性能优化及最佳实践 等多个角度,深入探讨 computed 和 watch 的区别,并结合项目实际案例,理解和应用 Vue 的响应式特性。
2. Computed(计算属性) 深入解析
2.1 Computed 的底层实现
在 Vue 2 的源码中,computed 是通过 Watcher 实现的,关键逻辑如下:
- 创建
computedWatcher实例 :当computed被定义时,Vue 内部会创建一个Watcher实例,并将lazy设置为true(惰性计算)。 - 依赖收集 :当
computed依赖的数据(响应式数据)变化时,该computedWatcher会被标记为dirty,但不会立即重新计算。 - 惰性计算 :当
computed计算属性被访问时,Vue 发现dirty = true,才会执行计算并缓存结果。 - 缓存机制 :如果
dirty = false,则直接返回上次计算的结果,而不会重复计算。
2.2 Vue 2 源码解析(Computed 实现)
在 Vue 2 的 src/core/instance/state.js 文件中,computed 的初始化主要依赖 defineComputed() 函数:
csharp
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作为缓存标志,只有在依赖变化时才会重新计算。
2.3 Computed 的性能优势
- 避免不必要的重复计算 :由于
computed具有缓存特性,即使被多次访问,只要依赖未变化,都不会重新计算,从而提升性能。 - 基于依赖更新,而非主动触发 :相比
watch,computed仅在依赖变更时更新 ,避免了watch可能引起的额外副作用执行。
计算属性缓存 vs 方法调用
xml
<template>
<p>计算属性:{{ reversedMessage }}</p>
<p>方法调用:{{ reverseMessage() }}</p>
</template>
<script>
export default {
data() {
return {
message: "Hello Vue"
};
},
computed: {
reversedMessage() {
console.log("计算属性执行");
return this.message.split('').reverse().join('');
}
},
methods: {
reverseMessage() {
console.log("方法执行");
return this.message.split('').reverse().join('');
}
}
};
</script>
运行结果
computed只会执行一次,后续访问都返回缓存值。methods每次调用都会重新计算,浪费性能。
3. Watch(侦听器)深入解析
3.1 Watch 的底层实现
watch 在 Vue 2 中同样依赖 Watcher 进行依赖追踪,核心机制包括:
- 创建
Watcher实例 :watch监听的数据变化时,Vue 触发Watcher实例执行回调函数。 - 同步执行 vs 异步执行 :Vue 2 默认使用异步更新策略 ,
watch在组件更新之后执行(nextTick队列)。 - 深度监听(Deep Watching) :
watch默认是浅监听 ,如果监听的是对象或数组,需要设置deep: true进行递归监听。
源码解析(Vue 2 watch 的实现)
在 Vue 2 的 src/core/observer/watcher.js 文件中,watch 的核心实现如下:
kotlin
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)当数据变化时执行用户提供的回调函数。
3.2 Watch 的性能挑战
- 每次变更都会执行回调,即使数据计算结果没有变化。
- 深度监听可能造成性能问题,因为 Vue 需要遍历整个对象树来检查变化。
Watch 深度监听的性能问题
javascript
watch(
() => user,
(newVal, oldVal) => console.log("用户数据变化", newVal),
{ deep: true }
);
如果 user 对象非常庞大,Vue 需要递归遍历所有属性,会增加性能开销。
4. Computed vs Watch 对比
| 特性 | Computed(计算属性) | Watch(侦听器) |
|---|---|---|
| 缓存 | 是(有缓存) | 否(无缓存) |
| 执行时机 | 访问时触发计算 | 依赖数据变化后立即执行 |
| 适用场景 | 计算派生数据 | 监听数据并执行副作用 |
| 支持异步 | 否 | 是 |
| 深度监听 | 否 | 是(deep: true) |
5. 进阶优化与最佳实践
5.1 结合 Computed 和 Watch 进行优化
在许多实际场景中,我们可以结合 computed 和 watch 进行优化:
javascript
const fullName = computed(() => `${user.firstName} ${user.lastName}`);
watch(fullName, (newVal) => {
console.log("用户姓名变化:", newVal);
});
6. 结论
computed适用于计算派生数据,具备缓存特性,提升性能。watch适用于监听数据变化并执行副作用(如 API 请求、手动 DOM 操作)。computed应优先使用,只有在computed不能满足需求时才使用watch。- 避免
watch监听整个对象,结合computed提高效率。