跳到主要内容

认识

2024年03月18日
柏拉文
越努力,越幸运

一、认识


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: 标识这是一个 refcomputed 也算作 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() 重新计算,而是简单地将 computedRefdirty 标记为 true。这样,computed惰性 的, 只有在下次读取 computedRef.value 时才会重新计算。

四、依赖追踪与缓存更新: 1. 缓存, 当 Computedgetter 被执行时, 其返回值被存储在 _value 中。如果在此后依赖没有变化, 则 _value 会直接复用,避免了重复计算。2. 依赖追踪, 当 Computed 内部依赖的响应式数据更新后(例如在 getter 内部通过 reactive 对象的 get 拦截器触发 track``),scheduler 会执行, 将 Computeddirty 标记设为 true,并通过 triggerRefValue 通知所有依赖该 computedeffect(例如在模板中使用该 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 访问器会进行拦截, 如果此时的 dirtytrue, 开始执行 effect 的返回值 effectFn 也就是 computed 传入的 getter 计算结果。并收集与这个 computed 相关的副作用函数。

4.2 effect

computed 函数接受一个 getter 作为参数, 将 getter 函数作为副作用函数, 创建一个带有 lazyschedulareffect。 传入的配置如下:

const effect = effect(getter,{
lazy: true,
scheduler(){

}
});
  1. lazy 的作用是懒执行 effect 传入的回调

4.3 getter

computed 函数接受一个 getter 作为参数, 将 getter 函数作为副作用函数, 创建一个带有 lazyschedulareffect。此时 , effect 会返回一个函数 effectFn, 这个函数的内部会执行 getter

4.4 schedular

schedulereffect 的调度器, 可以控制副作用函数的执行顺序与执行次数。如果 effect 存在 scheduler 配置项, 当响应式数据发生变化时, 会触发 scheduler 调度函数执行, 而不是直接触发副作用函数。

五、问题

5.1 computed vs watch

watch 用于监测响应式数据, 当数据发生变化时, 通知并执行相应的回调函数。watch 传入的第一个参数为要监测的响应式数据, watch 中创建一个 effect 副作用, 第一个参数为封装好副作用函数, 这个副作用函数主要是读取传入的响应式数据, 触发响应式数据的副作用收集机制, 传入 Scheduler 配置项, 在 effect 中, 如果有 Scheduler 配置项, 那么执行 Scheduler 调度函数执行, 而不是直接触发副作用函数执行。在 Scheduler 调度函数中, 执行传入的 watch 回调。watch 有三个参数, 侦听器的源, 发生变化的回调, 和一个配置项, 可以配置 immediate 立即监听, 可以配置 deep 进行深度监听。 在变化的回调中, 同样提供了三个参数, 最新值, 旧值, 以及一个用于注册副作用清理的回调函数 cleanupcleanup 可以用于解决一些竟态问题, 在响应式数据发生变化后, 调用 cleanup, 并传入想要取消的逻辑代码, 优先取消之前的副作用影响后,再开始新的副作用。

computed 是一个计算属性, 根据传入的 getter 函数, 计算得到一个可读或者可读可写的响应式数据。 computed 本质上是将传入的 getter 作为一个副作用函数, 创建一个带有 lazyschedulereffect, 返回一个具有访问器属性value设置器属性value 的数据。在读取 computed 数据时, 访问器进行拦截, 在内部变量 dirtytrue 的情况下, 执行通过 effect 返回的 effectFn 函数, 这个 effectFn 内部会执行 computed 传入的 getter 进行计算。因此, computedref 一样, 是一个新的状态数据, 并具有延时计算和缓存计算结果的功能。