计算属性
// 1. 响应式数据
const data = reactive({ count: 0 });
// 2. 计算属性
const plusOne = computed(() => data.count + 1);
// 3. 依赖收集
effect(() => console.log(plusOne.value));
// 4. 触发上面的effect重新执行
data.count++;
为什么data.count
的改变能间接触发访问了计算属性的 effect 的重新执行呢?
首先看一下简化版本的computed
的代码
export function computed(getter) {
let dirty = true;
let value: T;
// 这里还是利用了effect做依赖收集
const runner = effect(getter, {
// 这里保证初始化的时候不去执行getter
lazy: true, //表示effect函数返回的runner并不会立即执行,懒加载
computed: true, // 表示这是一个computed effect,用于trigger阶段的优先级排序
// 调度执行顺序的实现
scheduler: () => {
// 在触发更新时 只是把dirty置为true
// 而不去立刻计算值 所以计算属性有lazy的特性
dirty = true;
},
});
return {
get value() {
if (dirty) {
// 在真正的去获取计算属性的value的时候
// 依据dirty的值决定去不去重新执行getter 获取最新值
value = runner();
dirty = false;
}
// 这里是关键 后续讲解
trackChildRun(runner);
return value;
},
set value(newValue: T) {
setter(newValue);
},
};
}
首先要知道,effect 函数会立即开始执行,再执行之前,先把
effect自身
变成全局的activeEffect
,以供响应式数据收集依赖。并且
activeEffect
的记录是用的栈的方式,随着函数的开始进行入栈,随着函数的结束出栈。这样就可以维护嵌套的effect关系。
先起几个别名便于讲解
// 计算effect
const plusOne =computed(() => data.count + 1);
// 日志effect
effect(() => console.log(plusOne.value));
从依赖关系来看,
日志effect
读取了计算effect
计算effect
读取了响应式属性count
所以更新的顺序也应该是:
count改变` -> `计算effect更新` -> `日志effect更新
解读
当日志effect开始执行的时候,此时的activeEffect
是日志effect,此时的 effectStack
是[ 日志 effect ]
执行日志effect读取了plusOne.value,进而触发了
get value() {
if (dirty) {
// 在真正的去获取计算属性的value的时候
// 依据dirty的值决定去不去重新执行getter 获取最新值
value = runner()
dirty = false
}
// 这里是关键 后续讲解
trackChildRun(runner)
return value
},
首先进入了求值过程:value = runner(),runner 其实就是计算effect,它是对于用户传入的 getter 函数的包装
进入runner()函数之后,此时的activeEffect就是计算effect,此时的 `effectStack` 是[ 日志 effect, 计算 effect]
runner 所包裹的() => data.count + 1
也就是计算effect
会去读取count
,因为是由 effect 包裹的函数,所以触发了响应式数据的get
拦截:
此时count
会收集计算effect
作为自己的依赖。
并且计算effect
会收集count
的依赖集合,保存在自己身上。(通过effect.deps
属性)
dep.add(activeEffect);
activeEffect.deps.push(dep);
也就是形成了一个双向收集的关系,于是计算effect有了count的所有依赖,count
也存了计算effect
的依赖。
然后在 runner 运行结束后,计算effect
出栈了,此时activeEffect
变成了栈顶的日志effect
此时activeEffect
是日志 effect,此时的effectStack
是[ 日志 effect ]
接下来进入关键的步骤:`trackChildRun()`
trackChildRun(runner);
function trackChildRun(childRunner: ReactiveEffect) {
for (let i = 0; i < childRunner.deps.length; i++) {
const dep = childRunner.deps[i];
dep.add(activeEffect);
}
}
这个runner
就是计算effect
,它的deps
上此时挂着count
的依赖集合,
在trackChildRun
中,它把当前的 activeEffect 也就是日志effect
也加入到了count
的依赖集合中。
此时count
的依赖集合是这样的:[ 计算effect, 日志effect ]
这样下次count
更新的时候,会把两个 effect 都重新触发,而由于触发的顺序是先触发computed effect
后触发普通effect
,因此就完成了
- 计算 effect 的 dirty 置为 true,标志着下次读取需要重新求值。
- 日志 effect 读取计算 effect 的 value,获得最新的值并打印出来。