vue:手动实现一个相对完善的响应式系统(四)

67 阅读4分钟

7.computed原理

利用前面的响应系统,就可以实现vue最有特色的computed,在这之前,还有一个知识就是关于懒执行的effec,即lazy的effect.什么是懒执行的effect

    effect(()=>{
        console.log(obj.foo)//这个函数会立即执行
    })

在一些场景下,我们并不希望它立即执行,而是等到我们需要时再执行,例如计算属性.可以通过options中添加lazy属性达到目的

    effect(()=>{//指定lazy选项,函数不会立即执行
        console.log(obj.foo)
    },
    {//options
        lazy:true
        
    })

按照这lazy,修改effect函数的实现逻辑,当lazy为true时,不立即执行副作用函数

    function effect(fn,options={}){
        const effectFn=()=>{
            cleanup(effectFn)
            activeEffect=effectFn
            effectStack.push(effectFn)
            fn()
            effectStack.pop()
            activeEffect = effectStack[effectStack.length-1]
        }
        effectFn.options=options
        effectFn.deps=[]
        //只有非lazy时才执行
        if(!options.lazy){
            effectFn()
        }
        //将副作用函数返回
        return effectFn
    }

这样副作用函数就不会立即执行,我们返回了副作用函数,这样我们就可以控制副作用函数什么时候执行了

    const effectFn=(()=>{
        console.log(obj.foo)
    },{lazy:true})
    //手动执行副作用函数
    effectFn()

我们还可以把传递给effect函数看作一个getter,这样这个getter函数可以返回任何值

    const effectFn=effect(()=>obj.foo+obj.bar,{lazy:true})

我们手动执行副作用函数时,就可以拿到值

   const effectFn=effect(()=>obj.foo+obj.bar,{lazy:true})
   const value = effectFn()

我们这里还需要对effec函数做修改

    function effect(fn,options={}){
        const effectFn=()=>{
            cleanup(effectFn)
            activeEffect=effectFn
            effectStack.push(effectFn)
            const res=fn() //将fn的执行结果存进res中
            effectStack.pop()
            activeEffect = effectStack[effectStack.length-1]
            return res
        }
        effectFn.options=options
        effectFn.deps=[]
        //只有非lazy时才执行
        if(!options.lazy){
            effectFn()
        }
        //将副作用函数返回
        return effectFn
    }

可以从代码看出,fn是真正的副作用函数,effectFn是包装过的副作用函数,通过调用fn返回的结果作为effectFn调用返回的结果,才能实现getter 接下来就是实现计算属性

    function computed(getter){
        //把getter作为副作用函数,创建一个lazy的effect
        const effectFn=effect(getter,{lazy:true})
    }
    cosnt obj={
        get value(){
        //读取value时才执行effectFn
            return effectFn()
        }
    }
    retuen obj

这里我们定义了一个computed函数,将getter作为参数,把getter作为副作用函数,创建一个lazy的effect,computed函数返回一个对象,对象中value属性是一个访问器属性,只有读取value值,才会执行effectFn将其结果返回 利用computed函数创建一个计算属性

    const data = {foo:1,bar:2}
    const obj = new Proxy(data,{})
    const sums = computed(()=>obj.foo+obj.bar)
    console.log(sums.value) //3

这个计算属性还有缺陷,当我们多次访问,这个计算属性会进行多次计算,没有对值进行缓存,下面就是对computed增加缓存功能

    function computed(getter){
        //value用来缓存上一次的值
        let value
        //标记,为true时,则需要计算
        let dirty
        const effectFn = effect(getter,{
            lazy:true
        })
        
        const obj={
            get value(){
                //当dirty为true时,需要重新计算,将新值存进value中
                if(dirty){
                    value = effectFn()
                    //将dirty设为false,下一次直接访问value中的值
                    dirty=false
                }
                return value
            }
        }
        return obj
    }

这段代码问题很明显,dirty什么时候设置为true,先看这样写有什么问题

    const data = {foo:1,bar:2}
    const obj = new Proxy(data,{})
    const sums = computed(()=>obj.foo+obj.bar)
    console.log(sums.value) //3  正常输出
    //修改obj.foo
    obj.foo++
    console.log(sums.value) //3 依然还是3
    

这是因为第一次访问sums.value时,dirty设置为false,代表不需要计算了,我们修改了obj.foo,里面的dirty依然为false,不执行副作用函数,也就不更新新值了,所以当obj.foo和obj.bar修改时,dirty要重置为true,前面的调度器scheduler就可以实现

    function computed(getter){
        //value用来缓存上一次的值
        let value
        //标记,为true时,则需要计算
        let dirty
        const effectFn = effect(getter,{
            lazy:true,
            scheduler(){
                dirty = true
            }
        })
        
        const obj={
            get value(){
                //当dirty为true时,需要重新计算,将新值存进value中
                if(dirty){
                    value = effectFn()
                    //将dirty设为false,下一次直接访问value中的值
                    dirty=false
                }
                return value
            }
        }
        return obj
    }

我们添加了调度器,当getter所依赖数据发生变化时,执行调度函数,将dirty设为true,下一次访问sums.value就会调用effectFn计算. 现在的计算属性并不完美,当发生下面这个情况

    const sumRes = computed(()=>obj.foo+obj.bar)
    effect(()=>{
        //在副作用函数中读取sumres.value
        console.log(sumRes.value)
    })
    修改obj.foo的值
    obj.foo++

sumRes是一个计算属性,正常逻辑来说,修改sumRes的值是会触发副作用函数执行,但是上面代码并不会因为numRes改变而执行,这时,我们需要在读取计算属性时,手动调用track函数追踪,进行依赖收集

    function computed(getter){
        //value用来缓存上一次的值
        let value
        //标记,为true时,则需要计算
        let dirty
        const effectFn = effect(getter,{
            lazy:true,
            scheduler(){
                dirty = true
                //依赖数据发生变化,调用trigger函数触发响应
                triggrt(obj,'value')
            }
        })
        
        const obj={
            get value(){
                //当dirty为true时,需要重新计算,将新值存进value中
                if(dirty){
                    value = effectFn()
                    //将dirty设为false,下一次直接访问value中的值
                    dirty=false
                }
                //读取value时,调用track函数追踪
                track(obj,'value')
                return value
            }
        }
        return obj
    }

新增的两行代码其实就是为这个sumRes补一下get()和set()操作,和proxy代理对象原理一样