computed计算属性是存在依赖关系,当依赖的值发生变化时计算属性也随之变化
案例
首先引入reactive、effect、computed三个函数,声明obj响应式数据和computedObj计算属性,接着执行effect函数,该函数传入了一个匿名函数进行computedObj的赋值,最后两秒后修改obj的name值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, effect, computed } = Vue
// 创建响应式数据
const obj = reactive({
name: 'jc'
})
// 计算属性 触发 obj.name 的 get 行为
const computedObj = computed(() => {
return '姓名:' + obj.name
})
// effect 函数中 触发 计算属性的 get 行为
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
})
// 修改响应式数据的 name 值 触发 set 行为
setTimeout(() => {
obj.name = 'cc'
}, 2000)
</script>
</body>
</html>
ComputedRefImpl 实例
computed函数定义在packages/reactivity/src/computed.ts文件下:

该函数接收 getterOrOptions 参数,即我们传入的匿名函数() => { return '姓名:' + obj.name }:

之后赋值给getter,setter我们可以理解为一个空函数,之后创建一个 ComputedRefImpl 实例,并将其返回,看下ComputedRefImpl构造函数:
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
public _dirty = true // 脏变量 关键
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self) // 依赖收集
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
该构造函数会创建一个ReactiveEffect实例,这块逻辑就不再赘述,先看下返回的实例effect:

另外还需关心 ReactiveEffect 传入的第二个参数 scheduler,该构造函数在packages/reactivity/src/effect.ts文件下:
scheduler我们可以理解为一个调度器,这也是**computed** 核心所在,该逻辑会进行依赖触发
// 传入的参数
() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
}
// ReactiveEffect 构造函数
export class ReactiveEffect<T = any> {
// 省略
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
// 省略
}
ComputedRefImpl定义了一个_dirty脏变量,还定义了get value和set value两个方法,这也是和ref相同,赋值时需带上.value属性的原因
get value 会进行依赖收集,但是依赖触发并没有在 set value 中,而是在我们之前 ReactiveEffect 传入的第二个参数中
此时computed函数执行完毕返回ComputedRefImpl实例对象:

之后执行effect函数,进行赋值document.querySelector('#app').innerHTML = computedObj.value,从而触发computed的get value方法:

get/set
trackRefValue(self)进行依赖收集,该方法之前也讲到过。由于此时_dirty脏变量为true(默认为true),所以之后设置为 false ,再执行 self.effect.run() 进行赋值
effect.run()实际执行的是fn()方法,即computed传入的匿名函数() => { return '姓名:' + obj.name },effect函数执行完毕,页面呈现如下:

两秒后触发obj的setter行为,即执行createSetter方法进行trigger依赖触发(第一次),然后根据name属性获取到对应的effects,该逻辑都在packages/reactivity/src/effect.ts文件下:

之后triggerEffects函数遍历effects,执行triggerEffect(effect, debuggerEventExtraInfo):

这里需要关注下if (effect.scheduler)判断逻辑,由于此时执行的**effect** 含有 computed 属性,且存在 scheduler,则会执行effect.scheduler()方法:

这就是之前提到的ComputedRefImpl构造函数中,创建ReactiveEffect实例时传入的第二个参数:
export class ComputedRefImpl<T> {
// 省略
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
})
// 省略
}
// 省略
}
// 第二个参数
() => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // dirty 为 false 时 触发依赖
}
}
因为在computed函数的get value方法中,_dirty设置了false,所以直接走判断逻辑,将 _dirty 设置为 false ,执行 triggerRefValue(this) 依赖触发(第二次) ,所以computed的依赖触发是在该逻辑中执行的,这里是关键
此时获取到的effects:

所以根据判断逻辑直接走 effect.run() ,执行run等于执行fn方法,即执行effect传入的匿名函数,之后执行document.querySelector('#app').innerHTML = computedObj.value赋值操作,再次触发computed的get value方法:
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self) // 依赖收集
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
接着执行self._value = self.effect.run()!,又再次执行computed传入的匿名函数() => { return '姓名:' + obj.name }重新赋值:

代码执行完成,此时页面呈现修改后的值:

总结
1. computed 计算属性实际是一个 ComputedRefImpl 构造函数的实例
ComputedRefImpl构造函数中通过_dirty变量来控制effect中run方法的执行和triggerRefValue的触发
- 初始时
_dirty为true,表示计算属性的值尚未计算,需要重新计算 - 当值被计算并缓存后,
_dirty设置为false - 当计算属性的依赖发生变化时,例如响应式数据被修改,
_dirty重新设置为true,表示需要重新计算值
2.想要访问计算属性的值,必须通过 .value ,因为它内部和 ref 一样是通过 get value 来进行实现的
每次.value时都会执行get value方法,从而触发trackRefValue进行依赖收集
3. 在依赖触发时,先触发**computed的effect** ,再触发非**computed的effect**
1)避免依赖循环和死锁
当计算属性和普通 effect 之间存在双向依赖时,若同时触发两者的更新,可能导致无限循环。例如:计算属性 A 的更新触发了普通 effect B,B 的修改又触发了 A 的依赖更新
先计算属性,后普通 effect 可以打破这种循环,计算属性的更新完成后,普通 effect 再基于最新的计算值执行,从而避免循环
2)确保计算属性的值是最新状态
计算属性的核心是缓存机制,其值需要基于依赖项的最新状态计算。若普通 effect 先执行,可能导致计算属性在后续访问时仍使用旧的缓存值(即脏状态未更新),从而产生错误结果
3)性能优化
计算属性的更新通常涉及复杂计算,而普通 effect 可能包含 DOM 操作等耗时任务。通过分阶段触发:
- 减少重复计算:若普通 effect 先触发并修改了响应式数据,可能导致计算属性多次重新计算
- 合并更新:计算属性的更新完成后,普通 effect 可以基于最终状态一次性完成渲染,减少重复渲染
2010

被折叠的 条评论
为什么被折叠?



