乐于分享
好东西不私藏

Vue3计算属性与侦听器源码级解析

Vue3计算属性与侦听器源码级解析

从响应式系统核心到实际开发避坑,一文打尽computed/watch/watchEffect

上期我们聊了组件系统的奥秘,这期换个角度,深入Vue3两个最常用却也最容易被误用的API——computedwatch。你有没有遇到过这种场景:computed值明明没变,模板却疯狂刷新?watch监听不到对象属性的变化?或者解构出来的数据突然”失去灵魂”变成响应式孤儿?这些问题,本质上都源于对Vue3响应式系统底层机制的认知盲区。今天我们就从源码出发,彻底理解它们的工作原理。

01

computed的缓存机制:从dirty标记到scheduler调度

先问个问题:computed为什么能缓存结果?很多同学知道computed有”缓存”,但这个缓存是怎么工作的?答案是dirty标记。当你第一次访问computed时,Vue会执行getter并记录结果,同时把dirty设为false。只有当依赖项变化时,scheduler才会把dirty重新标记为true,下次访问才会重新计算。

来看看Vue3源码中computed的核心实现(基于@vue/reactivity包):

class ComputedRefImpl<T> {
private _value: T
public effect: ReactiveEffect
private _dirty: boolean = true // 核心:脏标记

get value() {
// 收集依赖:track阶段
trackEffects(this.effect)

// 缓存逻辑:未脏则直接返回
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
}

关键点在于:当依赖变化时,Vue不会立即重新计算,而是通过scheduler将dirty标记设为true。这就像图书馆的”待还书”标签——书变了,但等你真正去借(访问)时才更新。

图1:computed缓存机制原理 – dirty标记的流转过程

scheduler的调度策略也很精妙。Vue3使用队列机制批量处理副作用,同一tick内的多次依赖变化只会触发一次重算。这是懒更新策略——不是你变我就算,而是等我(指computed)真正需要的时候再算。

02

computed vs watch:性能差异的根源

很多场景下computed和watch似乎可以互换,但它们的性能模型完全不同。computed是”订阅-推送”模式,依赖变化时被动通知;watch则是”主动轮询”模式,需要你明确指定要监听什么。

💡 实战经验

能用computed解决的场景,千万别用watch。computed有缓存优化,watch每次变化都会执行回调。

watch的本质是注册一个立即执行的effect。Vue3的doWatch源码核心逻辑如下:

// Vue3 watch 实现核心(简化版)
function doWatch(
source: WatchSource, // 监听源
cb: WatchCallback | null, // 回调函数
options: WatchOptions // 配置项
) {
const scheduler = new SchedulerJob()

// 创建effect,immediate决定是否立即执行
const effect = new ReactiveEffect(
source, // getter函数
scheduler // 变化时触发scheduler
)

// 深度监听时,遍历递归收集依赖
if (options.deep) {
traverse(source()) // 强制访问所有嵌套属性
}

// 立即执行一次(immediate: true)
if (options.immediate) {
cb(undefined, source())
}

return cleanup // 取消监听函数
}

图2:watchEffect与watch的机制差异对比

03

watchEffect与watch:不是选择困难,是场景驱动

watchEffect本质上就是不支持回调的watch,它自动收集依赖,但无法获取变化前后的值。什么时候用哪个?我的经验法则是:

  • 需要旧值→ 必须用watch,watchEffect给不了你变化前的状态
  • 只关心副作用执行→ watchEffect更简洁,不用声明依赖
  • 需要深度监听→ watch的deep选项 vs watchEffect自动深层追踪
  • 需要防抖控制→ watch的debounce vs watchEffect需自己包装

深度监听是个性能陷阱。deep: true意味着Vue会递归遍历对象的所有属性,这在大型嵌套对象场景下是噩梦。如果只需要监听特定路径,优先使用getter函数精确指定。

// ❌ 低效:对整个form深度监听
watch(form, handler, { deep: true })

// ✅ 高效:只监听特定字段(Vue3.4+语法糖)
watch(
() => form.name, // 只追踪name属性
(newVal, oldVal) => {
console.log(newVal, oldVal)
}
)
04

computed执行时序:为什么有时候缓存不生效

来看个常见疑惑:为什么我的computed没有缓存效果,每次都重新计算?

图3:computed三种执行场景的时序对比

答案藏在getter函数内部。如果getter返回的是函数或响应式代理,每次访问computed.value返回的是新引用,Vue无法判断内容是否变化。另外,在watchEffect中访问computed也会产生额外的追踪开销。

// ❌ 每次返回新数组,缓存失效
const list = computed(() => [1, 2, 3].filter(n => n > item.value))

// ✅ 使用shallowRef或返回原始引用
const list = computed(() => originList.value.filter(n => n > item.value))
05

实战:响应式数据丢失的常见场景与排查

这是最近星球里问得最多的问题之一:为什么我明明定义了一个reactive对象,解构出来的值却”死”了?先看看原理:

图4:解构导致响应式丢失的原理与解决方案

reactive解构丢失响应的本质是:解构拿到的是原始值副本,而不是代理对象。当你在setup中解构后,模板访问的其实是普通JavaScript值,失去了Proxy的追踪能力。

import { reactive, toRefs, toRef } from 'vue'

// ❌ 解构丢失响应式
const state = reactive({
name: 'Vue3',
version: 3.4
})

// name 变成普通值,不再响应式
const { name} = state

// ✅ 方案1:toRefs保持响应式
const stateRef = toRefs(state)
// 使用时需要 stateRef.name.value

// ✅ 方案2:toRef保持单个属性响应式
const nameRef = toRef(state, 'name')

// ✅ 方案3:Vue3.4+ reactive解构(推荐)
const { name, version } = state // 自动保持响应式

🔥 Vue3.4 新特性

reactive解构语法让你可以直接解构reactive对象而不会丢失响应式,但注意:解构出来的变量不能重新赋值,否则会破坏响应式追踪。

06

最佳实践建议

总结几条实战中验证过的最佳实践:

  • computed优先原则:能用computed计算的,坚决不用watch。computed有缓存优化,watch每次变化都执行。
  • 避免深度监听:尽量用getter精确指定监听路径,deep: true是性能杀手。
  • immediate慎用:immediate: true的watch首屏会执行两次(初始化+首次变化),注意副作用处理。
  • watch返回值记得清理:组件卸载时自动停止监听,但如果你需要在逻辑中手动停止,保存返回值并调用。
  • computed的getter要纯净:不要在getter里执行有副作用的操作(如修改外部状态),这会破坏响应式系统的可预测性。
// ✅ 最佳实践:合理的watch使用
const stop = watch(
() => searchQuery.value, // 精确指定依赖
async (query) => {
const results = await fetchAPI(query)
dataList.value = results
},
{ debounce: 300 as any } // 防抖处理
)

// 组件卸载时自动停止,或手动停止
onUnmounted(() => stop())

核心要点回顾:
computed通过dirty标记实现懒计算,watch通过effect+scheduler实现主动监听。
能用computed绝不用watch,深度监听是性能陷阱,Vue3.4的reactive解构是解药。

📚 延伸阅读

想深入Vue3响应式系统?可以阅读《Vue3 Design》和@vue/reactivity源码。
下期预告:Vue3依赖注入provide/inject源码解析,敬请期待~

觉得有收获?别忘了点赞+在看,我们下期见~