vue3源码分析 -- computed
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 可以基于最终状态一次性完成渲染,减少重复渲染