您的位置:首页 > 技术中心 > 前端框架 >

Vue3响应式系统怎么实现computed

时间:2023-05-15 14:28

首先,我们简单回顾一下:

响应式系统的核心就是一个 WeakMap --- Map --- Set 的数据结构。

Vue3响应式系统怎么实现computed

WeakMap 的 key 是原对象,value 是响应式的 Map。这样当对象销毁的时候,对应的 Map 也会销毁。

Map 的 key 就是对象的每个属性,value 是依赖这个对象属性的 effect 函数的集合 Set。然后用 Proxy 代理对象的 get 方法,收集依赖该对象属性的 effect 函数到对应 key 的 Set 中。还要代理对象的 set 方法,修改对象属性的时候调用所有该 key 的 effect 函数。

上篇文章我们按照这样的思路实现了一个比较完善的响应式系统,然后今天继续实现 computed。

实现 computed

首先,我们把之前的代码重构一下,把依赖收集和触发依赖函数的执行抽离成 track 和 trigger 函数:

Vue3响应式系统怎么实现computed

逻辑还是添加 effect 到对应的 Set,以及触发对应 Set 里的 effect 函数执行,但抽离出来清晰多了。

然后继续实现 computed。

computed 的使用大概是这样的:

const value = computed(() => {    return obj.a + obj.b;});

对比下 effect:

effect(() => {    console.log(obj.a);});

区别只是多了个返回值。

所以我们基于 effect 实现 computed 就是这样的:

function computed(fn) {    const value = effect(fn);   return value}

当然,现在的 effect 是没有返回值的,要给它加一下:

Vue3响应式系统怎么实现computed

只是在之前执行 effect 函数的基础上把返回值记录下来返回,这个改造还是很容易的。

现在 computed 就能返回计算后的值了:

Vue3响应式系统怎么实现computed

但是现在数据一遍,所有的 effect 都执行了,而像 computed 这里的 effect 是没必要每次都重新执行的,只需要在数据变了之后执行。

所以我们添加一个 lazy 的 option 来控制 effect 不立刻执行,而是把函数返回让用户自己执行。

Vue3响应式系统怎么实现computed

然后 computed 里用 effect 的时候就添加一个 lazy 的 option,让 effect 函数不执行,而是返回出来。

computed 里创建一个对象,在 value 的 get 触发时调用该函数拿到最新的值:

Vue3响应式系统怎么实现computed

我们测试下:

Vue3响应式系统怎么实现computed

可以看到现在 computed 返回值的 value 属性是能拿到计算后的值的,并且修改了 obj.a. 之后会重新执行计算函数,再次拿 value 时能拿到新的值。

只是多执行了一次计算,这是因为 obj.a 变的时候会执行所有的 effect 函数:

Vue3响应式系统怎么实现computed

这样每次数据变了都会重新执行 computed 的函数来计算最新的值。

这是没有必要的,effect 的函数是否执行应该也是可以控制的。所以我们要给它加上调度的功能:

Vue3响应式系统怎么实现computed

可以支持传入 schduler 回调函数,然后执行 effect 的时候,如果有 scheduler 就传给它让用户自己来调度,否则才执行 effect 函数。

这样用户就可以自己控制 effect 函数的执行了:

Vue3响应式系统怎么实现computed

然后再试一下刚才的代码:

Vue3响应式系统怎么实现computed

可以看到,obj.a 变了之后并没有执行 effect 函数来重新计算,因为我们加了 sheduler 来自己调度。这样就避免了数据变了以后马上执行 computed 函数,可以自己控制执行。

现在还有一个问题,每次访问 res.value 都要计算:

Vue3响应式系统怎么实现computed

能不能加个缓存呢?只有数据变了才需要计算,否则直接拿之前计算的值。

当然是可以的,加个标记就行:

Vue3响应式系统怎么实现computed

scheduler 被调用的时候就说明数据变了,这时候 dirty 设置为 true,然后取 value 的时候就重新计算,之后再改为 false,下次取 value 就直接拿计算好的值了。

我们测试下:

Vue3响应式系统怎么实现computed

我们访问 computed 值的 value 属性时,第一次会重新计算,后面就直接拿计算好的值了。

修改它依赖的数据后,再次访问 value 属性会再次重新计算,然后后面再访问就又会直接拿计算好的值了。

至此,我们完成了 computed 的功能。

但现在的 computed 实现还有一个问题,比如这样一段代码:

let res = computed(() => {    return obj.a + obj.b;});effect(() => {    console.log(res.value);});

我们在一个 effect 函数里用到了 computed 值,按理说 obj.a 变了,那 computed 的值也会变,应该触发所有的 effect 函数。

但实际上并没有:

Vue3响应式系统怎么实现computed

这是为什么呢?

这是因为返回的 computed 值并不是一个响应式的对象,需要把它变为响应式的,也就是 get 的时候 track 收集依赖,set 的时候触发依赖的执行:

Vue3响应式系统怎么实现computed

我们再试一下:

Vue3响应式系统怎么实现computed

现在 computed 值变了就能触发依赖它的 effect 了。至此,我们的 computed 就很完善了。

完整代码如下:

const data = {    a: 1,    b: 2}let activeEffectconst 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()        }    })}

以上就是Vue3响应式系统怎么实现computed的详细内容,更多请关注Gxl网其它相关文章!

热门排行

今日推荐

热门手游