一、前言
在上一篇中,我们已经实现了一个基础的响应式系统。如果不知道怎么构建一个基础的响应式系统,深入剖析 Vue 响应式系统:从零实现一个精简版 。现在,我们将在此基础上,手动实现 Vue 的 computed 函数。
源码地址:github.com/chuyuan132/...
二、了解 computed 的核心特性
要实现 computed,首先要理解它的两个核心特性:
- 懒执行 (Lazy Evaluation) :只有当你真正访问
computed属性的值时,它才会执行计算。 - 缓存 (Caching) :如果依赖的响应式数据没有发生变化,
computed会返回上一次缓存的值,而不是重新计算。这大大提升了性能。
computed 的实现会借用我们之前实现的 effect 函数。如果不了解effect如何实现的,请浏览深入剖析 Vue 响应式系统:从零实现一个精简版。为了满足懒执行的特性,我们需要改造 effect 函数,增加一个 lazy 选项。
三、改造 effect 函数以支持懒执行
在我们之前的 effect 函数中,只要调用 effect(fn),副作用函数 fn 就会立即执行。为了实现懒执行,我们可以添加一个 lazy 选项。如果 lazy 为 true,effect 函数将不再立即执行 fn,而是直接返回 effectFn 本身。
javascript
// ...(省略其他代码)
const defaultOptions = {
scheduler: null,
lazy: false,
};
function effect(fn, options = defaultOptions) {
function effectFn() {
// ...(省略原有逻辑)
const res = fn(); // 副作用函数执行,返回其返回值
// ...(省略原有逻辑)
return res;
}
effectFn.deps = [];
effectFn.options = options;
if (!options.lazy) {
// 如果不是懒执行,则立即执行
effectFn();
}
// 无论是否立即执行,都返回 effectFn
return effectFn;
}
四、实现 computed 函数
有了支持懒执行的 effect 函数,我们就可以着手实现 computed 了。computed 函数接受一个 getter 函数作为参数,并返回一个对象,该对象有一个 value 属性。
1. 基础结构
首先,我们利用 effect 的 lazy 选项来创建 computed 的基本结构。
javascript
function computed(getter) {
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
// 访问 value 时,才执行 effectFn
return effectFn();
},
};
return obj;
}
这段代码已经实现了懒执行,但还缺少缓存功能。
2. 添加缓存机制
为了实现缓存,我们需要一个变量来追踪 computed 是否需要重新计算。我们称这个变量为 dirty。
- 当
dirty为true时,表示值是"脏"的,需要重新计算。 - 当
dirty为false时,表示值是干净的,可以返回缓存值。
同时,我们还需要一个 value 变量来保存缓存的结果。
javascript
function computed(getter) {
let dirty = true;
let value = undefined;
const effectFn = effect(getter, { lazy: true });
const obj = {
get value() {
// 只有当 dirty 为 true 时才重新计算
if (dirty) {
// 执行 effectFn 并将结果缓存到 value
value = effectFn();
// 重新计算后,将 dirty 设为 false
dirty = false;
}
return value;
},
};
return obj;
}
这段代码实现了缓存,但 dirty 一旦变为 false 就再也没有机会变回 true 了。我们如何让它在依赖变化时重新变为 true 呢?
3. 依赖变化时重新计算
当 getter 函数内部的响应式依赖发生变化时,dirty 应该被重置为 true。这里,scheduler 调度器就派上用场了。
我们可以给 effect 函数传入一个 scheduler 选项。当 getter 的依赖变化时,scheduler 函数会被调用。我们可以在这个函数里将 dirty 设为 true。
javascript
function computed(getter) {
let dirty = true;
let value = undefined;
const effectFn = effect(getter, {
lazy: true,
scheduler: () => {
// 依赖变化时,将 dirty 设为 true
dirty = true;
},
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
return value;
},
};
return obj;
}
4. 解决嵌套副作用函数的依赖问题
现在,如果我们在另一个 effect 函数中访问 computed 的 value,会发生什么?
javascript
const name = createProxy({ firstName: 'Jack', lastName: 'Chen' });
const fullName = computed(() => `${name.firstName} ${name.lastName}`);
effect(() => {
// 当 fullName.value 变化时,这个副作用函数应该重新执行
console.log(fullName.value);
});
// 依赖改变,fullName.value 应该更新
name.firstName = 'Mike';
当 name.firstName 改变时,computed 的 scheduler 会被调用,将 dirty 设为 true。但 effect 函数并不知道 fullName.value 的值变了,所以它不会重新执行。
为了解决这个问题,我们需要让 computed 能够像普通响应式数据一样被追踪。也就是说,当 computed 的值发生变化时,它应该能够触发依赖它的副作用函数。
我们可以在 computed 的 get value() 内部进行 依赖收集(track) ,并在 scheduler 中 触发依赖(trigger) 。
javascript
// ... (省略其他代码)
// 优化后的 computed 函数
function computed(getter) {
let dirty = true;
let value = undefined;
// 使用 effect 的 scheduler,当依赖变化时,触发 computed 的更新
const runner = effect(getter, {
lazy: true,
scheduler() {
if (!dirty) {
dirty = true;
// 触发 computed 值的依赖
trigger(proxy, 'value');
}
},
});
const obj = {
get value() {
if (dirty) {
value = runner();
dirty = false;
}
// 在访问 computed.value 时,进行依赖收集
track(proxy, 'value');
return value;
},
};
return obj
}
经过以上优化,我们成功地实现了一个完整的 computed 函数,它同时具备了懒执行 、缓存 和响应式的特性。
五、演示
现在,你可以用新的 computed 函数来测试一下,看看它是否能正常工作。
javascript
const obj = {
count: 0,
};
const proxyObj = createProxy(obj);
const result = computed(() => proxyObj.count + 1);
effect(() => {
console.log("访问计算属性的effect函数", result.value);
});
setTimeout(() => {
proxyObj.count = 2;
}, 1000);