Vue3 computed 的依赖追踪机制介绍
·
一、原理介绍
1.1 什么是computed
Vue 的核心能力是"数据驱动视图"——数据变了,视图自动更新。但视图不应该直接写数据,而应该从数据派生——因为派生关系意味着:
- 同一份数据可以派生多个视图(数据复用)
- 派生关系自动维护(数据变 → 所有派生值自动更新)
- 派生值可以被缓存(同一份数据不重复计算)
Vue 3 把这种派生能力封装成 computed(),这是 Vue 区别于 React 的关键设计之一。React 的派生要么写在 render 里(每次都执行)、要么用 useMemo(手动管理依赖),而 Vue 的 computed 自动追踪、自动缓存、自动失效。
1.2 响应式系统三件套
Vue 3的响应式系统由三部分组成:
(1) reactive() / ref()
用 Proxy 包装原始对象 / 值
拦截 get / set / has / deleteProperty 等操作
(2) effect()
收集依赖:在函数执行时把"被读取的属性"登记到 dep
触发更新:属性变化时把所有登记的 effect 重新执行
(3)dep (依赖容器)
订阅者列表,每个 reactive 属性都有独立的 dep
addSub / trigger / notify
简化版源码:
// reactive 用 Proxy 包装对象
function reactive(target) {
return new Proxy(target, {
get(target, key) {
track(target, key) // 收集依赖
return Reflect.get(target, key)
},
set(target, key, value) {
Reflect.set(target, key, value)
trigger(target, key) // 触发更新
return true
}
})
}
// track 把当前 effect 注册到属性的 dep
function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, depsMap = new Map())
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, dep = new Set())
dep.add(activeEffect)
}
}
// trigger 把属性的所有 effect 重新执行
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) dep.forEach(effect => effect.run())
}
1.3 computed 的本质
computed() 本质上是一个带缓存的 effect:
function computed(getter) {
let cachedValue
let dirty = true // 标记脏值,需要重新计算
const effect = createEffect(getter, () => {
if (!dirty) {
dirty = true
// 让依赖自己的 effect 也失效
trigger(computedObj, 'value')
}
})
const computedObj = {
get value() {
track(computedObj, 'value') // 谁读 computed 谁被收集
if (dirty) {
cachedValue = effect.run()
dirty = false
}
return cachedValue
},
set value(newValue) {
// 可选 setter
}
}
return computedObj
}
三个关键特性:
- 懒计算:第一次访问
.value才执行 getter - 自动缓存:依赖没变就不重算
- 依赖追踪:谁读了
.value谁就被登记为订阅者
1.4 依赖收集与缓存失效
模板渲染读取 .value
↓
activeEffect = 当前模板渲染 effect
↓
computed.get() 执行 getter
↓
getter 内部读取 reactive 属性(如 project.kcpNodes.kcp3.status)
↓
该属性的 get 被 Proxy 拦截
↓
track() 把当前 activeEffect 登记到该属性的 dep
↓
computed 缓存值,dirty = false
↓
下次访问 .value:如果 dirty = false 直接返回缓存
数据变化时:
↓
该属性的 set 被拦截
↓
trigger() 把 dep 里的 effect 重新执行
↓
computed 的 effect 标记 dirty = true,并 trigger 自己
↓
依赖 computed 的 effect(模板渲染)失效 → 下次渲染重新读 .value → 触发 getter 重算
关键点:computed 的依赖追踪是自动的——你写 getter 时正常读响应式数据就行,Vue 自动帮你记住"哪些属性变了会影响这个 computed"。
二、使用方法
2.1 基本语法
import { computed, ref } from 'vue'
const count = ref(0)
// 派生值:自动追踪、自动缓存
const double = computed(() => count.value * 2)
// 模板里访问(自动 unwrap)
console.log(double.value) // 0
count.value = 5
console.log(double.value) // 10(依赖变化触发重算)
2.2 computed 链:组合多个派生值
computed 之间可以互相依赖,形成派生链:
const a = ref(1)
const b = ref(2)
// 第一层:基础派生
const sum = computed(() => a.value + b.value)
// 第二层:基于第一层再派生
const isPositive = computed(() => sum.value > 0)
// 第三层:基于第二层派生
const label = computed(() => isPositive.value ? '正数' : '非正数')
// a 变化 → sum 重算 → isPositive 重算 → label 重算
// b 变化同理
a.value = -1
console.log(label.value) // '非正数'
响应式数据流:
a / b 变化
↓
sum dirty = true
↓
谁读 sum(isPositive)触发重算
↓
isPositive dirty = true
↓
谁读 isPositive(label)触发重算
↓
label dirty = true
↓
模板渲染重读 label.value
2.3 计算属性的 Setter
computed 默认只有 getter,但可以加 setter 实现"双向绑定":
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get: () => `${firstName.value}${lastName.value}`,
set: (newValue) => {
const parts = newValue.split('')
firstName.value = parts[0]
lastName.value = parts.slice(1).join('')
}
})
fullName.value = '李四'
console.log(firstName.value) // '李'
console.log(lastName.value) // '四'
2.4 computed 与 watch 的边界
| 场景 | 用 computed | 用 watch |
|---|---|---|
| 派生一个值供模板/其他 computed 使用 | ✓ | ✗ |
| 监听某个值变化后执行副作用(API 调用 / DOM 操作) | ✗ | ✓ |
| 同步派生(一个值变了立即重算) | ✓ | 部分场景可 |
| 异步派生(debounce / API 调用) | ✗ | ✓ |
| 派生值需要缓存 | ✓(自动缓存) | ✗ |
关键原则:
- computed 用于派生值——纯函数,无副作用
- watch 用于副作用——执行 API 调用 / 启动定时器 / 跳转路由
更多推荐
所有评论(0)