vue中,计算属性api主要是通过其所依赖的响应式数据来动态计算所需要的值。在Vue3中,它的本质上其实是一个函数,该函数返回一个ref对象,其value属性就是计算结果。
同时也可以传入一个配置对象,配置对象中包含一个get函数与一个set函数,分别用于计算与更新。
计算属性最重要的一点是它有缓存机制,就是当第一次计算出结果之后,如果所依赖的数据没有变化,在下一次使用的计算属性时就不会在重新计算了,而是从缓存中读取数据直接返回。
这个缓存的重点是在其所依赖的effect上设一个脏值dirty,当effect的run函数执行时,将dirty设为非,在下一次读取计算属性的值是需要判断其所依赖effect的dirty值,如果为true则重新计算,否则直接从缓存中读取。
默认为每个effect设置脏值属性为Dirty,并设置dirty 属性访问器与设置器,便于后续访问与设置
# constants.ts
export enum DirtyLevels {
Dirty = 4, // 脏值,意味着取值时需要重新计算
NoDirty = 0// 未脏,意味着取值时不需要重新计算
}
# effect.ts
export class ReactiveEffect {
...
_dirtyLevel = DirtyLevels.Dirty; // 当前effect的脏等级
...
// 脏值属性访问器
public get dirty() {
return this._dirtyLevel === DirtyLevels.Dirty;
}
// 脏值属性设置器
public set dirty(value) {
this._dirtyLevel = value ? DirtyLevels.Dirty : DirtyLevels.NoDirty
}
run () {
// 调用时设置脏值为NoDirty
this._dirtyLevel = DirtyLevels.NoDirty
....
}
}
computed函数传入一个参数,一般为一个配置对象,也可以是一个函数,如果是函数则将函数作为配置对的get函数
返回一个ref对象
# computed.ts
import { isFunction } from "@vue/shared";
export function computed(getterOrOptions) {
let getter = null;
let setter = null;
// 判断传入的配置对象是否是一个函数
if(isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = () => {}
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// 创建ref对象
return new ComputedRefImpl(getter,setter)
}
1. ComputedRefImpl和普通的ref一样需要设置属性访问器与设置器。同时在创建对象的时候需要将getter作为effect的run函数,创建一个effect对象,用于后续计算值
2. 本质上ComputedRefImpl最重要的就是属性访问器与effect依赖
3. 当访问属性时,通过调用effect.run来获取到结果并返回
4. 在get属性访问器中,优先判断efect是否是脏值,一但脏值就说明efferct.run函数还未调用,因此调用函数获取到结果并保存到_value中,再一次读取时就直接会从_value中读取,不需要再次调用effect.run函数
class ComputedRefImpl {
public _value;
public effect;
public dep;
constructor(getter,public setter) {
// 创建依赖
this.effect = new ReactiveEffect(() => getter(this._value), () => {
// TODO: 数据更新后出发依赖
})
}
get value() {
if(this.effect.dirty) {
this._value = this.effect.run();
// TODO : 如果当前在effect中访问了计算属性,需要将使用的effect收集为依赖
}
return this._value
}
set value(newValue) {
// 赋值
this.setter(newValue);
}
}
1. 当第一次计算结果时,我们需要将当前的effect对象收集为当前ref对象的依赖,本质上就是将其添加到ref.dep中,我们前面在开发ref模块时书写了一个将当前依赖添加到ref对象上的函数trackrRefValue,直接调用函数即可
2. 当计算属性依赖的数据更新后,就会触发this._effect的构造函数的第二个参数,因此我们需要在该函数中来进行触发更新,前面也写过一个函数,用户触发ref对象上的所有依赖triggerRefValue
3. 现在当依赖数据变化,就会通过triggerRefValue 触发ComputedRefImpl对象的所有依赖执行一遍,就做到了数据随着依赖值得变化而变化。
class ComputedRefImpl {
...
constructor(getter,public setter) {
// 创建依赖
this.effect = new ReactiveEffect(() => getter(this._value), () => {
triggerRefValue(this)
})
}
get value() {
if(this.effect.dirty) {
this._value = this.effect.run();
// 如果当前在effect中访问了计算属性,需要将使用的effect收集为依赖
trackrRefValue(this)
}
...
}
set value(newValue) {
...
}
}
现在还存在的一个问题就是虽然计算属性可以跟随依赖数据的变化而重新执行依赖,但由于我们未改变effect的脏值状态,当前的结果并未得到更新,因此我们需要在触发依赖的过程中将本ref上的所有effect的脏值全部设为Dirty,这样在接下来的依赖调用时就会重新计算结果
// 触发依赖
export function triggerEffect(deps) {
for(const effect of deps.keys()) {
effect._dirtyLevel = DirtyLevels.Dirty // 标记为脏值
if(effect.scheduler) {
// 判断当前是否已经有正在执行的effect
if(effect._running === 0) {
effect.scheduler() // 默认相当于调用了effect.run()
}
}
}
}
至此,计算属性模块基本完成,还存在的问题就是计算属性在被主动赋值是需要打印出来一个警告,目前没有实现