上篇文章我们实现了基本的响应式系统,这篇文章继续实现 computed。
首先,我们简单回顾一下:
响应式系统的核心就是一个 WeakMap --- Map --- Set 的数据结构。
WeakMap 的 key 是原对象,value 是响应式的 Map。这样当对象销毁的时候,对应的 Map 也会销毁。
Map 的 key 就是对象的每个属性,value 是依赖这个对象属性的 effect 函数的集合 Set。
然后用 Proxy 代理对象的 get 方法,收集依赖该对象属性的 effect 函数到对应 key 的 Set 中。
还要代理对象的 set 方法,修改对象属性的时候调用所有该 key 的 effect 函数。
上篇文章我们按照这样的思路实现了一个比较完善的响应式系统,然后今天继续实现 computed。
实现 computed
首先,我们把之前的代码重构一下,把依赖收集和触发依赖函数的执行抽离成 track 和 trigger 函数:
逻辑还是添加 effect 到对应的 Set,以及触发对应 Set 里的 effect 函数执行,但抽离出来清晰多了。
然后继续实现 computed。
computed 的使用大概是这样的:
const value = computed(() => {
return obj.a + obj.b;
});
- 1.
- 2.
- 3.
对比下 effect:
effect(() => {
console.log(obj.a);
});
- 1.
- 2.
- 3.
区别只是多了个返回值。
所以我们基于 effect 实现 computed 就是这样的:
function computed(fn) {
const value = effect(fn);
return value
}
- 1.
- 2.
- 3.
- 4.
- 5.
当然,现在的 effect 是没有返回值的,要给它加一下:
只是在之前执行 effect 函数的基础上把返回值记录下来返回,这个改造还是很容易的。
现在 computed 就能返回计算后的值了:
但是现在数据一变,所有的 effect 都执行了,而像 computed 这里的 effect 是没必要每次都重新执行的,只需要在数据变了之后执行。
所以我们添加一个 lazy 的 option 来控制 effect 不立刻执行,而是把函数返回让用户自己执行。
然后 computed 里用 effect 的时候就添加一个 lazy 的 option,让 effect 函数不执行,而是返回出来。
computed 里创建一个对象,在 value 的 get 触发时调用该函数拿到最新的值:
我们测试下:
可以看到现在 computed 返回值的 value 属性是能拿到计算后的值的,并且修改了 obj.a. 之后会重新执行计算函数,再次拿 value 时能拿到新的值。
只是多执行了一次计算,这是因为 obj.a 变的时候会执行所有的 effect 函数:
这样每次数据变了都会重新执行 computed 的函数来计算最新的值。
这是没有必要的,effect 的函数是否执行应该也是可以控制的。所以我们要给它加上调度的功能:
可以支持传入 schduler 回调函数,然后执行 effect 的时候,如果有 scheduler 就传给它让用户自己来调度,否则才执行 effect 函数。
这样用户就可以自己控制 effect 函数的执行了:
然后再试一下刚才的代码:
可以看到,obj.a 变了之后并没有执行 effect 函数来重新计算,因为我们加了 sheduler 来自己调度。这样就避免了数据变了以后马上执行 computed 函数,可以自己控制执行。
现在还有一个问题,每次访问 res.value 都要计算:
能不能加个缓存呢?只有数据变了才需要计算,否则直接拿之前计算的值。
当然是可以的,加个标记就行:
scheduler 被调用的时候就说明数据变了,这时候 dirty 设置为 true,然后取 value 的时候就重新计算,之后再改为 false,下次取 value 就直接拿计算好的值了。
我们测试下:
我们访问 computed 值的 value 属性时,第一次会重新计算,后面就直接拿计算好的值了。
修改它依赖的数据后,再次访问 value 属性会再次重新计算,然后后面再访问就又会直接拿计算好的值了。
至此,我们完成了 computed 的功能。
但现在的 computed 实现还有一个问题,比如这样一段代码:
let res = computed(() => {
return obj.a + obj.b;
});
effect(() => {
console.log(res.value);
});
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
我们在一个 effect 函数里用到了 computed 值,按理说 obj.a 变了,那 computed 的值也会变,应该触发所有的 effect 函数。
但实际上并没有:
这是为什么呢?
这是因为返回的 computed 值并不是一个响应式的对象,需要把它变为响应式的,也就是 get 的时候 track 收集依赖,set 的时候触发依赖的执行:
我们再试一下:
现在 computed 值变了就能触发依赖它的 effect 了。
至此,我们的 computed 就很完善了。
完整代码如下:
const data = {
a: 1,
b: 2
}
let activeEffect
const effectStack = [];
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn);
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
effectFn.deps = []
effectFn.options = options;
if (!options.lazy) {
effectFn()
}
return effectFn
}
function computed(fn) {
let value
let dirty = true
const effectFn = effect(fn, {
lazy: true,
scheduler(fn) {
if(!dirty) {
dirty = true
trigger(obj, 'value');
}
}
});
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value');
console.log(obj);
return value
}
}
return obj
}
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
const reactiveMap = new WeakMap()
const obj = new Proxy(data, {
get(targetObj, key) {
track(targetObj, key);
return targetObj[key]
},
set(targetObj, key, newVal) {
targetObj[key] = newVal
trigger(targetObj, key)
}
})
function track(targetObj, key) {
let depsMap = reactiveMap.get(targetObj)
if (!depsMap) {
reactiveMap.set(targetObj, (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(targetObj, key) {
const depsMap = reactiveMap.get(targetObj)
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()
}
})
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
总结
上篇文章我们实现了响应式的核心数据结构,依赖的收集、数据变化后通知依赖函数执行。今天我们在那基础上实现了 computed。
我们改造了 effect 函数,让它返回传入的 fn,然后在 computed 里自己执行来拿到计算后的值。
我们又支持了 lazy 和 scheduler 的 option,lazy 是让 effect 不立刻执行传入的函数,scheduler 是在数据变动触发依赖执行的时候回调 sheduler 来调度。
我们通过标记是否 dirty 来实现缓存,当 sheduler 执行的时候,说明数据变了,把 dirty 置为 true,重新计算 computed 的值,否则直接拿缓存。
此外,computed 的 value 并不是响应式对象,我们需要单独的调用下 track 和 trigger。
这样,我们就实现了完善的 computed 功能,vue3 内部也是这样实现的。