一、前言
在上一篇中,我们已经实现了一个基础的响应式系统。如果不知道怎么构建一个基础的响应式系统,深入剖析 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);