1.功能
接受一个 getter
函数,并根据 getter
的返回值返回一个不可变的响应式 ref
对象。
默认不执行,在取值时执行,具有缓存功能,数据不变多次取值只触发一次取值计算。
const state = reactive({ name: "orange" });
let aliasName = computed(() => {
console.log("执行计算方法");
return "_" + state.name;
});
console.log("开始取值111");
console.log(aliasName.value);
console.log(aliasName.value);
console.log(aliasName.value);
state.name = "apple";
console.log(aliasName.value);
2.原理:脏值检测机制**
计算属性本质是一个effect
,内部保存一个变量dirty
,用来判断是否需要重新执行。默认值为true
,需要执行,每次执行effect
后把dirty
改为false
,缓存执行结果,后续取值判断为false
,直接取缓存的值,依赖的属性发生变化后,dirty
改为true
3.实现 (vue 3.4版本)
console.log(aliasName)
3.1修改effect
在effect中增加一个标识_dirtyLevel
,初始值为DirtyLevels.Dirty
,在run()
方法中更新为DirtyLevels.NoDirty
,并提供出get
和set
方法
export const enum DirtyLevels {
Dirty = 4, // 脏值意味着要运行计算属性
NoDirty = 0, // 不脏就用上次的取值结果
}
export class ReactiveEffect {
...
public _dirtyLevel = DirtyLevels.Dirty; // 默认是脏的,计算属性运行过一次就不脏
constructor(public fn, private scheduler) {}
public get dirty() {
return this._dirtyLevel === DirtyLevels.Dirty;
}
public set dirty(value) {
this._dirtyLevel = value ? DirtyLevels.Dirty : DirtyLevels.NoDirty;
}
run() {
this._dirtyLevel = DirtyLevels.NoDirty; // 运行后有缓存不脏
...
}
3.2 判断computed参数
如果传递的是方法,则为get
方法,set
为空方法,如果传递是对象,则从中取出get
和set
方法
export function computed(getterOrOptions) {
let onlyGetter = isFunction(getterOrOptions);
let getter;
let setter;
if (onlyGetter) {
getter = getterOrOptions;
setter = () => {};
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
3.3实现依赖收集
class ComputedRefImpl {
public dep = undefined;
public effect = undefined;
__v_isRef = true; // 代表是ref,需要用.value 取值
public _value;
constructor(getter, public setter) {
// 此处不能使用effect 会立即执行,ReactiveEffect可以手动调用run方法执行
this.effect = new ReactiveEffect(
() => getter(this._value),
() => {
// 此方法为getter依赖的属性变化后执行的方法:scheduler,应该触发effect渲染
triggerRefValue(this);
}
);
}
get value() {
if (this.effect.dirty) {
this._value = this.effect.run();
// 如果当前在effect中访问了计算属性,计算属性可以收集effect
// 将当前的ref上的dep与activeEffect关联,完成收集
trackRefValue(this);
}
return this._value;
}
set value(value: any) {
this.setter(value);
}
}
3.4 实现数据更新
到上面为止,依赖的属性变化,会触发更新,但是数据还是原来的数据,如果我们需要在triggerEffects
中将effect._dirtyLevel = DirtyLevels.Dirty;
下次取值时判断为Dirty
,再次收集,执行
4.实现(Vue3初始版本)
import { isFunction } from "@vue/shared";
import { ReactiveEffect } from "./effect";
const noop = () => {};
class ComputedRefImpl {
dep = undefined;
effect = undefined;
__v_isRef = true; // 代表是ref,需要用.value 取值
_dirty = true;
_value;
constructor(getter, public setter) {
// 此处不能使用effect 会立即执行,ReactiveEffect可以手动调用run方法执行
this.effect = new ReactiveEffect(getter, () => {
// 此方法为getter依赖的属性变化后执行的方法:scheduler
this._dirty = true;
});
}
get value() {
if (this._dirty) {
this._value = this.effect.run();
this._dirty = false;
}
return this._value;
}
set value(value: any) {
this.setter(value);
}
}
export function computed(getterOrOptions) {
let onlyGetter = isFunction(getterOrOptions);
let getter;
let setter;
if (onlyGetter) {
getter = getterOrOptions;
setter = noop;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
return new ComputedRefImpl(getter, setter);
}
此时已经简单实现了computed,以下情况我们修改了state.name
的值,理论上应该执行effect
重新渲染页面,但是并未执行
const state = reactive({
name: "orange",
});
let aliasName = computed(() => {
console.log("执行计算方法");
return "_" + state.name;
});
effect(() => {
app.innerHTML = aliasName.value;
});
setTimeout(() => {
state.name = "apple";
}, 1000);
计算属性aliasName
收集了state.name
,effect
收集了计算属性aliasName
,同时让计算属性收集effect
,当计算属性依赖的属性变化后,不仅修改dirty
值,同时触发effect
get value() {
// 证明是在effect中使用的
if (activeEffect) {
trackEffects(this.dep || (this.dep = new Set()));
}
if (this._dirty) {
this._value = this.effect.run();
this._dirty = false;
}
return this._value;
}
this.effect = new ReactiveEffect(getter, () => {
// 此方法为getter依赖的属性变化后执行的方法:scheduler
this._dirty = true;
triggerEffects(this.dep);
});
调整effect.ts文件,抽出来方法trackEffects
和triggerEffects
const targetMap = new WeakMap();
export function track(target, key) {
// 如果取值操作没有发生在effect中,则不需要收集依赖
if (!activeEffect) {
return;
}
// 判断是否已经记录过
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
// 值为一个不重复数组
depsMap.set(key, (dep = new Set()));
}
trackEffects(dep);
}
export function trackEffects(dep) {
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep); //同时effect记住当前这个属性
}
}
export function trigger(target, key, newValue, oldValue) {
// 通过对象找到对应的属性 让这个属性对应的effect重新执行
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const dep = depsMap.get(key); // name 或者 age对应的所有effect
triggerEffects(dep);
}
export function triggerEffects(dep) {
const effects = [...dep];
// 运行的是数组 删除的是set
effects &&
effects.forEach((effect) => {
// 正在执行的effect ,不要多次执行
if (effect !== activeEffect) {
if (!effect.scheduler) {
effect.run();
} else {
effect.scheduler();
}
}
});
}