4.6 计算属性computed和lazy
懒执行的effect:一般的effect一下子就执行了,但是懒加载effect是等需要的时候才会执行
这时我们通过在options中添加lazy属性来达到目的
function effect (fn , options = {}) {
const effectFn = () => {
// 调用clearup函数完成清除工作
clearUp(effectFn);
activeEffect = effectFn;
// 在调用副作用函数之前将副作用函数压入栈中
effectStack.push(effectFn);
fn();
// 执行完之后抛出,把activeEffect还原成原来的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
// 将options挂在到fn上
effectFn.options = options;
// deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行副作用函数
if(!options.lazy){
effectFn();
}
return effectFn
}
那什么时候才执行这个函数呢?
因为我们前面返回了effectFn作为effect函数的返回值,那我们就可以手动调用该副作用函数了
const effectFn = effect(
() => {
console.log(obj.foo);
} ,
// 添加lazy
{
lazy : true
})
// 手动执行副作用函数
effectFn();
obj.foo++;
obj.foo++;
现在我们要实现假设传递给effect函数的是getter函数,可以返回getter函数的值
const effectFn = effect(
() => {
obj.foo + obj.bar
} ,
// 添加lazy
{
lazy : true
})
// 手动执行副作用函数
const value = effectFn();
console.log(value);
我们希望输出的是 obj.foo + obj.bar的值
实现computed函数
// computed函数
// 接收getter作为参数
function computed (getter) {
// 将gettwe作为副作用函数,创建一个lazy effect
const effectFn = effect( getter, {
lazy : true
})
const obj = {
// 对象的value是一个访问器属性
// 只有当读取value的值时,才会执行effctFn并将其作为结果返回
get value(){
return effectFn()
}
}
// 返回一个对象
return obj;
}
// 开始使用计算属性
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes);//3
但是有一个bug。多次访问会导致effectFn多次计算
解决—对值进行缓存
function computed (getter) {
// value用来缓存
let value
// dirty标志,标识是否需要重新计算
let dirty = true
// 将gettwe作为副作用函数,创建一个lazy effect
const effectFn = effect( getter, {
lazy : true
})
const obj = {
// 对象的value是一个访问器属性
// 只有当读取value的值时,才会执行effctFn并将其作为结果返回
get value(){
// 只有true时才可以进行计算
if(dirty){
value = effectFn()
// 将dirty设置为false,下一次直接使用缓存到value当中的值
dirty = false;
}
return value
}
}
// 返回一个对象
return obj;
}
无论我们执行了多少次sumRes都不会重新执行啦,直接取value里面的值
但是我们如果修改obj.foo的值,我们发现并没有响应的修改最后的sumRes
get value(){
// 只有true时才可以进行计算
if(dirty){
value = effectFn()
// 将dirty设置为false,下一次直接使用缓存到value当中的值
dirty = false;
}
return value
}
dirty = false;这便是原因
解决:当值发生变化的时候,改变dirty的值就可以啦,这时我们就要使用scheduler选项
// computed函数
// 接收getter作为参数
function computed (getter) {
// value用来缓存
let value
// dirty标志,标识是否需要重新计算
let dirty = true
// 将gettwe作为副作用函数,创建一个lazy effect
const effectFn = effect( getter, {
lazy : true,
scheduler(){
dirty = true
}
})
const obj = {
// 对象的value是一个访问器属性
// 只有当读取value的值时,才会执行effctFn并将其作为结果返回
get value(){
// 只有true时才可以进行计算
if(dirty){
value = effectFn()
// 将dirty设置为false,下一次直接使用缓存到value当中的值
dirty = false;
}
return value
}
}
// 返回一个对象
return obj;
}
// 开始使用计算属性
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value);//4
obj.foo++
console.log(sumRes.value);//5
我们的计算属性已经趋于完美了,但是还有一个问题
effect(() => {
console.log(sumRes.value)
})
obj.foo++
我们希望的是在完成 obj.foo++之后是可以重新渲染的,但是我们发现并没有
其实这是一个典型的effect嵌套,一个计算属性的内部有effect,并且它是懒执行的,只有当真正读取计算属性的值才会执行。
- 例如,假设我们有一个计算属性
fullName
,它依赖于响应式数据firstName
和lastName
。只有当我们读取fullName
的值时,才会执行计算fullName
的effect
。
但是getter里面访问的响应式数据只会把computed内部的effect收集为依赖。
而当把计算属性用于另一个effect时,就会发生effect嵌套,外层的effect不会被内层的effect所收集
- 例如,假设我们有一个
effect
函数updateUI
,它读取fullName
。这时,updateUI
的effect
和fullName
的effect
就会嵌套在一起。
解决—当读取计算属性的值时,我们可以手动调用track函数进行追踪,当计算属性发生变化的时候,手动调用trigger触发响应
- 当读取计算属性的值时,手动调用
track
函数,将当前effect
(如updateUI
)添加到计算属性的依赖列表中。 - 这样,当计算属性的依赖发生变化时,可以触发当前
effect
。 - 当计算属性的值发生变化时,手动调用
trigger
函数,通知所有依赖该计算属性的effect
进行更新。 - 这样,外层的
effect
(如updateUI
)会在计算属性(如fullName
)的依赖发生变化时被触发。
完整代码:
<script setup>
let activeEffect;
// effect栈
const effectStack = [];
function effect (fn , options = {}) {
const effectFn = () => {
// 调用clearup函数完成清除工作
clearUp(effectFn);
activeEffect = effectFn;
// 在调用副作用函数之前将副作用函数压入栈中
effectStack.push(effectFn);
// 将effect的值存储起来
const res = fn();
// 执行完之后抛出,把activeEffect还原成原来的值
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
// 返回res
return res;
}
// 将options挂在到fn上
effectFn.options = options;
// deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = [];
// 只有非lazy的时候才会执行副作用函数
if(!options.lazy){
effectFn();
}
return effectFn
}
const bucket = new WeakMap();
const data = { foo : 2 , bar : 2 }; // 确保所有属性都已定义
const obj = new Proxy(data, {
get(target, key){
track(target , key);
return target[key];
},
set(target, key, newVal){
target[key] = newVal;
trigger(target , key , newVal);
}
});
// 追踪变化
function track(target , key){
if(!activeEffect){
return;
}
// 根据tartget取来的depsMap,它是一个map类型
let depsMap = bucket.get(target);
// 如果不存在
if(!depsMap){
// 创建一个
bucket.set(target, (depsMap = new Map()));
}
// 根据key取来的deps,它是一个set类型
let deps = depsMap.get(key);
// 如果不存在
if(!deps){
// 创建一个
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect); // 添加当前活跃的副作用函数
activeEffect.deps.push(deps);
}
// 触发变化:处理调度逻辑
function trigger(target, key, newVal) {
const depsMap = bucket.get(target);
if (!depsMap) {
return;
}
const effects = depsMap.get(key);
// 开始执行
const effectsToRun = new Set(effects);
effects && effects.forEach(effectFn => {
if(activeEffect !== effectFn){
effectsToRun.add(effectFn);
}
});
effectsToRun.forEach(effectFn =>{
if(effectFn.options.scheduler){
effectFn.options.scheduler(effectFn);
}else {
effectFn();
}
});
// effects && effects.forEach(fn => fn()); // 只触发与键相关的副作用函数
}
// 清除函数
function clearUp (effectFn){
// 遍历然后进行删除
for(let i = 0 ; i < effectFn.deps.length ; i++){
const deps = effectFn.deps[i];
// 移除
deps.delete(effectFn);
}
// 最后重置effectFn.deps数组
effectFn.deps.length = 0;
}
// effect(() => {
// console.log(obj.foo);
// })
// obj.foo ++;
// obj.foo++;
// 连续执行同一代码,只饭后最后一次计算的结果
// 定义一个任务集合set:可以进行去重操作
const jobQueue = new Set();
// 使用promise.resolve()创建一个promise实例:将一个任务添加到微任务队列当中
const p = Promise.resolve();
// 开始刷新队列
let isFlushing = false;
function flushJob(){
// 如果队列正在被刷新->return
if(isFlushing){
return;
}
// 没有刷新,进行刷新操作
isFlushing = true;
p.then(() => {
jobQueue.forEach(job => job());
}).finally(() => {
// 结束后重置
isFlushing = false;
})
}
// computed函数
// 接收getter作为参数
function computed (getter) {
// value用来缓存
let value
// dirty标志,标识是否需要重新计算
let dirty = true
// 将gettwe作为副作用函数,创建一个lazy effect
const effectFn = effect( getter, {
lazy : true,
scheduler(){
// 设置value进行trigger
trigger(obj , 'value');
dirty = true
}
})
const obj = {
// 对象的value是一个访问器属性
// 只有当读取value的值时,才会执行effctFn并将其作为结果返回
get value(){
// 只有true时才可以进行计算
if(dirty){
value = effectFn()
// 将dirty设置为false,下一次直接使用缓存到value当中的值
dirty = false;
}
// 读取value,进行追踪
track(obj , 'value')
return value
}
}
// 返回一个对象
return obj;
}
// 开始使用计算属性
const sumRes = computed(() => obj.foo + obj.bar)
console.log(sumRes.value);
obj.foo++
console.log(sumRes.value);
effect(() => {
console.log(sumRes.value)
})
obj.foo
</script>
总结:
computed属性的实现我们首先用到了懒加载effect,需要使用的时候才使用。因为我们前面返回了effectFn作为effect函数的返回值,那我们就可以手动调用该副作用函数了。接着我们实现了computed属性,我们是传入一个getter函数和懒加载属性,为了解决多次访问会导致effectFn多次计算,我们需要缓存value。但是我们如果修改obj.foo的值,我们发现并没有响应的修改最后的sumRes,那是因为dirty并没有在修改值之后被修改为true,所以我们就要使用scheduler选项,在 effect
的 options
中添加 scheduler
,当计算属性的依赖发生变化时,将 dirty
设为 true
,以便下次读取 value
时重新计算。为了解决effect嵌套的问题,我们进行了手动追踪和触发