第十七章 vue3中computed计算属性的原理

99 阅读4分钟

computed和ref很像,都是变量.value,不一样的地方可能就是computed有缓存

首先我们先实现一个computed的happy path测试用例:

describe('computed',()=>{
    it("happy path",()=>{
        // 和ref很像变量.value
        // 缓存
        const user = reactive({
            age:1
        })

        const age = computed(()=>{
            return user.age
        })

        expect(age.value).toBe(1)
    })
})

实现这个用例就非常简单:

1 computed.ts中导出computed函数,然后computed函数接受一个getter函数

2 由于上面我们提到computed和ref很像,实现上其实也是通过computedImpl类来实现访问value属性的时候返回传入getter函数的返回值

再来实现第二个测试用例,这个测试用例有点长,我们分拆开来:

it("should compute lazily",()=>{
        const value = reactive({
            foo:1
        })
        const getter = jest.fn(()=>{
            return value.foo
        })
        const cValue = computed(getter)

        // lazy
        expect(getter).not.toHaveBeenCalled()

        expect(cValue.value).toBe(1)
        expect(getter).toHaveBeenCalledTimes(1)

        // should not computed again  这里需要把effect类进入进来 收集依赖
        cValue.value
        expect(getter).toHaveBeenCalledTimes(1)

        // should not compute until needed  trigger -> effect -> get 重新执行了 通过scheduler去实现不一值调用get,并且把_dirty变成true
        value.foo = 2
        expect(getter).toHaveBeenCalledTimes(1)

        // now it should computed
        expect(cValue.value).toBe(2)
        expect(getter).toHaveBeenCalledTimes(2)

        // should not computed again
        cValue.value
        expect(getter).toHaveBeenCalledTimes(2)

    })

在should not computed again之前的测试代码,在第一版的computed中是都可以通过的。到了下面后由于cValue.value中触发了getter,下面toHaveBeenCalledTimes就为2了。computed中是具有缓存功能的,但是由于我们代码中还没有为computed做缓存,所以他的结果就会为2,就不通过了。

接下来就为我们computed做一个缓存的功能,

给computed引入一个初始值为true的_dirty变量值,然后在get value中根据_dirty是否为true,判断是否为第一次访问get value,这样就可以通过单元测了。(这样就变成了只能调用一次 get value,再次调用的时候我们就把上次的值返回出去)

接下来我们分析一下should not compute until needed的测试代码,我们运行改测试代码时候就直接跑出来报错了,如图

image.png

其实这个报错是因为value.foo = 2的时候去走了trigger,触发依赖了,但是我们前面get中又没有对依赖进行一个收集,自然就undefined了。还有一个重要的点就是,当我们value.foo = 2后,我们下一次访问value的值需要进行一个更新的,但是我们此时的_dirty一直都是false的状态了,这个也是需要更改的。 (分析完后,其实就是我们需要对computed中get里做依赖收集,然后还需要在他trigger的时候去更新_dirty的变量)

解决没有收集依赖报错问题:

1 在effect.ts中将ReactiveEffect类导出,在computedImpl中声明一个this._effect去接收ReactiveEffect中new出来的对象

2 在ReactiveEffect中入getter

3 get value中的this._getter就是this._effect.run执行的返回值

image.png

当我们处理完上面的步骤以后,会发现又有一个新的报错

image.png

为什么getter会被执行了两次呢,主要是因为在我们响应式对象的值发生变化之后会去执行this._effect.run然后就会重新执行到getter,那要怎么样才能让他不执行两边getter,而且我们还需要在set完之后给this._drity变量变为true,这时候就想到了我们前面实现的scheduler,回顾一下scheduler是干什么的

scheduler: effect第一次执行走fn,当响应式对象发生set或update时候就,就走effect中传入的scheduler

image.png

其实真正写的代码并不多,只是computed的实现非常的巧妙,下面我先把涉及到的的代码放上来:

computed.ts

import { ReactiveEffect } from "./effect"

class computedImpl{
    private _getter: any
    private _dirty: boolean
    private _effect: any
    constructor(getter){
        this._getter = getter
        this._dirty = true
        this._effect = new ReactiveEffect(getter,()=>{
            if(!this._dirty)            {
                this._dirty = true
            }
        })

    }
    get value(){
        if(this._dirty){
            this._dirty = false
            this._getter = this._effect.run()
            return this._getter

        }
    }
}

export function computed(getter){
    return new computedImpl(getter)
}

effect.ts的代码就不贴了,因为他就讲ReactiveEffect类导了出去而已,如图

image.png

那这样看下来,其实代码也就computed.ts中的20几行,接下来总结一下他的整体逻辑

首先,我们computed里面是有一个effect的;

然后,我们调用get value的时候就去调用我们effect.run,这里使用了一个dirty的变量来判断有没有被调用过(这里做的就是computed的缓存功能);

最后,我们通过scheduler来让他改变响应式对象的时候,却重置dirty变量