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
提高效率。