大家好,我是小瑜。 半年时间转瞬即逝,仿佛昨天才刚敲响2024年新年钟声。试问自己这半年工作和学习进步了么,都收获了些什么?看着自己年初下定目标的完成率陷入了沉思。反思自己还是不够努力,一天天的积累会积少成多,但是天天的懒惰会导致自己的一事无成。在这么卷的现状下,需要适当的反思和自我批评。
自我批判结束!
通过前五章的学习,大致了解vue响应式系统,那么接下来就通过前面的知识,试着实现 computed 计算属性。
通过6个步骤依次实现及完善 computed, 搓搓手开始吧!
1. effect增加lazy
目前使用effect会立即执行,但是有些场景,希望需在需要的时候再执行 需要在effect中增加一个lazy属性 lazy 和之前的调度器一样,通过options选择对象执行
const data = { foo: 1 };
const effectFn = effect(
() => {
console.log(obj.foo);
},
{
lazy: true,
}
);
// 手动触发
btn.onclick = () => {
effectFn();
};
判断options中是否存在lazy,并控制是否执行 为了可以受控控制执行,需要将副作用函数返回,提供effect手动调用执行
// 添加options参数
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
effectFn.deps = [];
// 将 options 挂载到 effectFn 上
effectFn.options = options;
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn; //新增
}
2. 传递getter并返回任何值
希望可以传递一些计算方法,并返回计算过后的结果 例如希望的函数可以将传入的函数进行执行,并返回结果
import { effect, obj } from "./index.js";
const data = { foo: 1 ,bar:2 };
const obj = new Proxy(data,...)
const effectFn = effect(() => obj.foo + obj.bar, {
lazy: true,
});
btn.onclick = () => {
// value 是getter 的返回值 这里的结果为3
const value = effectFn();
console.log(value); // 3
};
可以将effect第一项也就是将 () => obj.foo + obj.bar 在effect中执行,并且将此结果返回出来,这样就可以得到目的,在之前effect基础上增加逻辑 下方代码1 和 2 中 即可完成这些逻辑的执行并且将结果返回
/**
* 通过新增代码可以看到,传递给effect函数的fn才是真正的副作用函数,
* 而effectFn是我们包装后的副作用函数,在effectFn中我们将fn的执行结果存储到res中
*/
export function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
// 1.将fn的执行结果存储到res中 新增
console.log(fn, "@@fn");
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
//2.将res作为effectFn的返回值 新增
return res;
};
effectFn.deps = [];
// 将 options 挂载到 effectFn 上
effectFn.options = options;
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn;
}
此时就可以获取到正确的结果 为3
3. 实现computed
通过给effect增加lazy以及传递getter并返回计算后的结果,此时就可以将这两处逻辑封装成计算属性 例如 将 () => obj.foo + obj.bar 作为getter传入给effect,并且将effect设置为lazy,即可完成
/**
* 计算属性
*/
function computed(getter) {
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
console.log("执行effect 获取计算结果");
return effectFn();
},
};
return obj;
}
const sumRes = computed(() => obj.foo + obj.bar);
console.log(sumRes.value, "@@sumRes"); // 结果为3
4. computed增加缓存
这里已经实现了一个计算属性,但是尝试着将 sumRes.value 多次读取 发现每次都会执行 effectFn ,即使依赖的值没有发生变化,也就是说现在的 computed 并没有缓存相同的值。这里通过打印来说明问题
const sumRes = computed(() => obj.foo + obj.bar);
console.log(sumRes.value, "@@sumRes"); // 执行effect 获取计算结果 结果为3
console.log(sumRes.value, "@@sumRes"); // 执行effect 获取计算结果 结果为3
console.log(sumRes.value, "@@sumRes"); // 执行effect 获取计算结果 结果为3
所以需要给 computed 在依赖没有发生变化的情况下增加缓存 这里需要设置两个变量 分别是 value 用来计算上一次的计算结果 dirty 是否需要重新执行effect
/**
* 实现计算属性
*/
function computed(getter) {
// 用来缓存上一次计算的值
let value;
// 用来表示是否需要重新计算值,true代表需要重新计算
let dirty = true;
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
if (dirty) {
console.log("执行effect");
value = effectFn();
// 计算完之后,将dirty设置为false,下次访问直接使用缓存到value中的值
dirty = false;
}
return value;
},
};
return obj;
}
此时不论执行多少次 在依赖相同的情况下 effect只会执行第一次 后续只是在读取value的值 这里通过使用不同的依赖进行多次执行,现在 computed 的value 已被成功缓存
const sumRes1 = computed(() => obj.foo + obj.bar);
const sumRes2 = computed(() => obj.foo - obj.bar);
console.log(sumRes1.value, "@@sumRes1"); // 执行effect 3
console.log(sumRes1.value, "@@sumRes1"); // 3
console.log(sumRes2.value, "@@sumRes2"); // 执行effect 2
console.log(sumRes2.value, "@@sumRes2"); // 2
5. 解决修改响应式数据不重新计算问题
用代码来说下现在的问题
const sumRes = computed(() => obj.foo + obj.bar);
console.log(sumRes.value, "@@sumRes"); // 执行effect 3
console.log(sumRes.value, "@@sumRes"); // 3
console.log(sumRes.value, "@@sumRes"); // 3
btn.onclick = () => {
obj.foo++;
console.log(sumRes.value, "@@sumRe222s"); // 3
};
问题出在 obj.foo 已经自增了,但是计算属性并没有更新最新的值,还是 结果还是3 而不是期望的4 问题出在 当修改响应式数据时,不会重复计算,因为computed内部设置了缓存,dirty为false无法进行effect 解决方案是: 我们为effect添加了调度器,它会在getter函数中所以来的响应式数据发生变化时执行, 这样我们可以在调度器中将dirty重置为true,当下一次修改响应式时,就会重新运行计算属性 这样既可以保证数据的缓存,也可以保证只要修改副作用就可以重新触发effect执行
/**
* 实现计算属性
*/
export function computed(getter) {
// 用来缓存上一次计算的值
let value;
// 用来表示是否需要重新计算值,true代表需要重新计算
let dirty = true;
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 执行effect时,会触发调度器的执行,这里就可以将dirty重置为true
scheduler() {
dirty = true;
},
});
const obj = {
get value() {
if (dirty) {
console.log("执行effect");
value = effectFn();
// 计算完之后,将dirty设置为false,下次访问直接使用缓存到value中的值
dirty = false;
}
return value;
},
};
return obj;
}
6. 解决effect嵌套导致时不更新问题
例如下方代码,期望的是当修改响应式数据后,effect可以及时获取到最新的计算属性值,但是当前sumRes的值始终为 3 并没有获取最新的值
const sumRes = computed(() => obj.foo + obj.bar);
effect(() => {
console.log(sumRes.value, "@@sumRes");
});
btn.onclick = () => {
obj.foo++;
};
原因是因为这里出现了effect的嵌套,导致没有收集相同且最新的的依赖,导致更新始终为3 要解决这个问题,可以在 get 读取的时候 手动的添加 track(obj, "value"); 并且当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应式。
export function computed(getter) {
// 用来缓存上一次计算的值
let value;
// 用来表示是否需要重新计算值,true代表需要重新计算
let dirty = true;
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 1.添加调度器,在调度器中奖dirty重置为true
scheduler() {
dirty = true;
// 2. 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应式
trigger(obj, "value");
},
});
const obj = {
get value() {
if (dirty) {
console.log("执行effect");
value = effectFn();
// 计算完之后,将dirty设置为false,下次访问直接使用缓存到value中的值
dirty = false;
}
// 3. 当读取 value 时,手动调用 track 函数进行追踪
track(obj, "value");
return value;
},
};
return obj;
}
此时就可以正确的触发effect执行
完整代码
const data = { foo: 1, bar: 2 };
let activeEffect;
const effectStack = [];
const bucket = new WeakMap();
/**
* 通过新增代码可以看到,传递给effect函数的fn才是真正的副作用函数,
* 而effectFn是我们包装后的副作用函数,在effectFn中我们将fn的执行结果存储到res中
*/
export function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
// 将fn的执行结果存储到res中 新增
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
//将res作为effectFn的返回值 新增
return res;
};
effectFn.deps = [];
// 将 options 挂载到 effectFn 上
effectFn.options = options;
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn();
}
// 将副作用函数作为返回值返回
return effectFn; //新增
}
export const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
return true;
},
});
function track(target, key) {
if (!activeEffect) return target[key];
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects);
effectsToRun.forEach((effectFn) => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}
function cleanup(fn) {
fn && fn.deps.forEach((dep) => dep.delete(fn));
fn.deps.length = 0;
}
/**
* 新增 scheduler 调度器
* 通过set 将任务添加到调度器任务队列中自动去重
*/
// 调度器任务队列
export const jobQueue = new Set();
const p = Promise.resolve();
let isFlushing = false;
export function flushJob() {
if (isFlushing) return;
isFlushing = true;
p.then(() => {
jobQueue.forEach((job) => job());
}).finally(() => {
isFlushing = false;
// jobQueue.length = 0;
});
}
/**
* 实现计算属性
*/
export function computed(getter) {
// 用来缓存上一次计算的值
let value;
// 用来表示是否需要重新计算值,true代表需要重新计算
let dirty = true;
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中奖dirty重置为true
scheduler() {
dirty = true;
// 当计算属性依赖的响应式数据发生变化时,手动调用trigger函数触发响应式
trigger(obj, "value");
},
});
const obj = {
get value() {
if (dirty) {
console.log("执行effect");
value = effectFn();
// 计算完之后,将dirty设置为false,下次访问直接使用缓存到value中的值
dirty = false;
}
// 当读取 value 时,手动调用 track 函数进行追踪
track(obj, "value");
return value;
},
};
return obj;
}
以上就是 computed 的实现过程。 是不是感觉也不是特别的难,基本都是结束effect 来进行二次封装。 关于effect的实现过程,感兴趣的同学可以翻阅前四章节。