认识
一、认识
Vue.js 3.0
中, computed
是一个计算属性, 根据传入的 getter
函数, 计算得到一个可读或者可读可写的响应式数据。Computed
具有惰性计算、缓存更新等特点。在 Vue 3
中, Computed
的实现是建立在响应式系统和 ReactiveEffect
之上的一种缓存计算机制, 主要依赖于 ReactiveEffect
来包装计算函数, 并利用调度器和 dirty
标记以及 _value
实现惰性计算与缓存更新。这种设计既保证了 Computed
只在必要时重新计算,又能高效地追踪依赖,实现高性能的响应式数据更新,是 Vue 3
响应式系统的重要组成部分。
一、Computed
底层结构
class ComputedRefImpl {
constructor(getter, setter, isReadonly) {
this.getter = getter;
this.setter = setter;
this._value = undefined;
this.dirty = true;
this.effect = new ReactiveEffect(getter, () => {
if (!this.dirty) {
this.dirty = true;
triggerRefValue(this);
}
});
this.__v_isRef = true;
this[ReactiveFlags.IS_READONLY] = isReadonly;
}
get value() {
trackRefValue(this);
if (this.dirty) {
this._value = this.effect.run();
this.dirty = false;
}
return this._value;
}
set value(newVal) {
if (this.setter) {
this.setter(newVal);
} else {
console.warn('Write operation failed: computed value is readonly.');
}
}
}
二、属性方法:
-
_value
: 缓存计算后的结果。 -
dirty
: 一个布尔标记,指示当前缓存是否失效。当依赖变化时会被标记为true
,表示下次读取需要重新计算。 -
effect
: 内部创建的ReactiveEffect
, 用于运行计算函数,并通过scheduler
实现依赖变化时标记computed
为脏。 -
__v_isRef
: 标识这是一个ref
(computed
也算作ref
),使得computed
对象具有统一的访问接口(即通过.value
访问)。 -
value getter
: 当外部访问 computedRef.value 时,首先会检查 dirty 标记。如果为 true,则调用 effect.run() 重新计算,并更新 _value;否则直接返回 _value。 -
value setter
(可选):如果用户为computed
提供了setter
,则computedRef
实现setter
,调用用户自定义的更新函数;如果没有setter
,则在尝试修改时会给出警告或不允许修改。
三、惰性计算: Computed
内部创建的 ReactiveEffect
会把计算函数包裹进去, 同时传入一个调度器函数(scheduler``)。当计算函数中依赖的响应式数据更新时,scheduler
不会立即调用 effect.run()
重新计算,而是简单地将 computedRef
的 dirty
标记为 true
。这样,computed
是 惰性 的, 只有在下次读取 computedRef.value
时才会重新计算。
四、依赖追踪与缓存更新: 1. 缓存, 当 Computed
的 getter
被执行时, 其返回值被存储在 _value
中。如果在此后依赖没有变化, 则 _value
会直接复用,避免了重复计算。2. 依赖追踪, 当 Computed
内部依赖的响应式数据更新后(例如在 getter
内部通过 reactive
对象的 get
拦截器触发 track``),scheduler
会执行, 将 Computed
的 dirty
标记设为 true
,并通过 triggerRefValue
通知所有依赖该 computed
的 effect
(例如在模板中使用该 computed
的组件),使得它们在下一次访问 computed.value
时重新计算。
二、类细节
2.1 class
get value
class Test{
get value(){
}
}
const test = new Test();
console.log(test.value);
set value
class Test{
set value(newValue){
}
}
const test = new Test();
test.value = xxx;
三、变量细节
3.1 value
value
通过 value
保存 effectFn()
的结果, 也就是存储上一次的计算值。
3.2 dirty
dirty
通过 dirty
控制 effectFn
是否执行、重新计算, 从而实现了 computed
的缓存。访问 computed
的值多次, 只会在第一次访问时进行真正的计算, 后续访问都会直接读取缓存的 value
值。也就是控制 computed
是否要重新计算。
四、函数细节
4.1 get
当访问 computed.value
时, get
访问器会进行拦截, 如果此时的 dirty
为 true
, 开始执行 effect
的返回值 effectFn
也就是 computed
传入的 getter
计算结果。并收集与这个 computed
相关的副作用函数。
4.2 effect
computed
函数接受一个 getter
作为参数, 将 getter
函数作为副作用函数, 创建一个带有 lazy
和 schedular
的 effect
。 传入的配置如下:
const effect = effect(getter,{
lazy: true,
scheduler(){
}
});
lazy
的作用是懒执行effect
传入的回调
4.3 getter
computed
函数接受一个 getter
作为参数, 将 getter
函数作为副作用函数, 创建一个带有 lazy
和 schedular
的 effect
。此时 , effect
会返回一个函数 effectFn
, 这个函数的内部会执行 getter
。
4.4 schedular
scheduler
为 effect
的调度器, 可以控制副作用函数的执行顺序与执行次数。如果 effect
存在 scheduler
配置项, 当响应式数据发生变化时, 会触发 scheduler
调度函数执行, 而不是直接触发副作用函数。
五、问题
5.1 computed vs watch
watch
用于监测响应式数据, 当数据发生变化时, 通知并执行相应的回调函数。watch
传入的第一个参数为要监测的响应式数据, watch
中创建一个 effect
副作用, 第一个参数为封装好副作用函数, 这个副作用函数主要是读取传入的响应式数据, 触发响应式数据的副作用收集机制, 传入 Scheduler
配置项, 在 effect
中, 如果有 Scheduler
配置项, 那么执行 Scheduler
调度函数执行, 而不是直接触发副作用函数执行。在 Scheduler
调度函数中, 执行传入的 watch
回调。watch
有三个参数, 侦听器的源, 发生变化的回调, 和一个配置项, 可以配置 immediate
立即监听, 可以配置 deep
进行深度监听。 在变化的回调中, 同样提供了三个参数, 最新值, 旧值, 以及一个用于注册副作用清理的回调函数 cleanup
。 cleanup
可以用于解决一些竟态问题, 在响应式数据发生变化后, 调用 cleanup
, 并传入想要取消的逻辑代码, 优先取消之前的副作用影响后,再开始新的副作用。
computed
是一个计算属性, 根据传入的 getter
函数, 计算得到一个可读或者可读可写的响应式数据。 computed
本质上是将传入的 getter
作为一个副作用函数, 创建一个带有 lazy
和 scheduler
的 effect
, 返回一个具有访问器属性value
和 设置器属性value
的数据。在读取 computed
数据时, 访问器进行拦截, 在内部变量 dirty
为 true
的情况下, 执行通过 effect
返回的 effectFn
函数, 这个 effectFn
内部会执行 computed
传入的 getter
进行计算。因此, computed
同 ref
一样, 是一个新的状态数据, 并具有延时计算和缓存计算结果的功能。