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

94 阅读4分钟

8.watch的实现原理

vue中的watch,每一个用vue进行开发的都熟悉不过来,那其中的原理又是怎么实现的呢? 实际上,还是运用了effect以及options.scheduler选项,下面是一个简单的watch函数实现

    //接受两个函数,source是响应式数据,cb是回调函数
    function watch(source,cb){
        effect(()=>{
            //触发读取操作
            ()=>source.foo,
        },{
            scheduler(){
                //数据变化时,执行cb函数
                cb()
            }
        })
    }

使用

    const data = {foo:1}
    const obj = new Proxy(data,{...})
    
    watch(()=>obj,()=>{
        console.log('变化了')
    })
    obj.foo++

上面只是简单的watch,采用了硬编码的方式,只能观测到obj.foo的变化,为了让watch具有通用性,还要封装一个通用的读取操作

    function watch(source,cb){
        effect(()=>{
            //调用traverse 递归读取
            ()=>traverse(source)
        },{
            scheduler(){
                cb()
            }
        })
    }
    
    //traverse函数
    function traverse(value,seen=new Set()){
        //如果读取的数据是原始值,或者被读取过,就什么都不用做
        if(typeof value !-== 'object' || value==null || seen.has(value)) return
        //将数据添加进seen中,代表读取过
        seen.add(value)
        //暂时不考虑数组等其他结构
        //如果value是一个对象,使用for...in 读取对象的值,并递归调用traverse进行处理
        for(cosnt k in value){
            traverse(value[k],seen)
        }
        return value
    }

watch函数除了观测响应式数据,还可以接受getter函数,在gettr函数里面,用户可以指定watch依赖哪些响应式数据,只有这些数据发生变化,才会触发回调函数执行

    watch(()=>obj.foo,()=>{
        console.log('变化了')
    })
    function watch(source,cb){
        //定义getter
        let getter
        //如果source是函数,说明用户传递是getter,直接把source赋值给getter
        if(typeof source === 'function') getter = source
        else getter = ()=>traverse(source) // 否则调用traverse递归读取
        
        effect(()=>{
            ()=>getter()
        },{
            scheduler(){
                cb()
            }
        })
    }

watch还有一个非常重要的功能没有实现,就是在回调函数中拿到新值和旧值

    function watch(source,cb){
        //定义getter
        let getter
        //如果source是函数,说明用户传递是getter,直接把source赋值给getter
        if(typeof source === 'function') getter = source
        else getter = ()=>traverse(source) // 否则调用traverse递归读取
        
        //定义新值和旧值
        let oldValue,newValue
        //使用effect注册副作用函数时,开启lazy选项,将返回值存储到effectFn中以便手动调用
        
        const effectFn =  effect(()=>{
            ()=>getter()
        },{
            lazy:true
            scheduler(){
            //在scheduler中重新执行副作用函数,得到是新值
                newValue = effectFn()
                //将新值和旧值作为回调函数的参数
                cb(newValue,oldValue)
                //更新旧值
                oldValue = newValue
            }
        })
        //手动调用副作用函数,拿到旧值
        oldValue = efffectFn
    }    

9.立即执行的回调

在vue中,可以使watch中的回调立即执行

    watch(()=>obj.foo,{
        console.log('变化了')
    },{
        immediate:true
    })

当immediate为true时,回调函数会在watch创建时执行一次,所以要再一次封装scheduler函数,分别在初始化时和变更时执行

    function watch(source,cb,options={}){
        //定义getter
        let getter
        //如果source是函数,说明用户传递是getter,直接把source赋值给getter
        if(typeof source === 'function') getter = source
        else getter = ()=>traverse(source) // 否则调用traverse递归读取
        
        //定义新值和旧值
        let oldValue,newValue
        //使用effect注册副作用函数时,开启lazy选项,将返回值存储到effectFn中以便手动调用
        
        //提取scheduler调度函数为一个独立的job函数
        cosnt job=()=>{
                newValue = effectFn()
                //将新值和旧值作为回调函数的参数
                cb(newValue,oldValue)
                //更新旧值
                oldValue = newValue
        }
        
        const effectFn =  effect(()=>{
            ()=>getter()
        },{
            lazy:true
            scheduler:job
        })
        
        if(options.immediate){
            //当immediate为true时,立即执行job
            job()
        }else{
        //手动调用副作用函数,拿到旧值
        oldValue = efffectFn        
        }

    }    

这样就实现了函数第一次执行 还有一个问题,我们在watch中发送请求,如果数据改变两次,发送两次请求,因为第2次请求比第一次慢,会把第一次的结果覆盖第二次次请求的值.在vue中,vue给我们提供了解决方案,就是用onInvalidate注册一个回调

    watch(obj,async(newValue,oldValue,onInvalidate)=>{
    
         // expired为false时,代表这个副作用函数没有过期
        let expired = false
        //调用onInvalidate()函数注册一个过期回调
        onInvalidate=(()=>{//过期时,将expired设置为true
            expired=true
        })
        //执行异步函数
        const res = await fetch('/path/request')
        if(!expired){
        //只有副作用函数没有过期,才会执行后面操作
            console.log(res)
        }
    })

那onInvalidate原理是什么

    function watch(source,cb,options={}){
        //定义getter
        let getter
        //如果source是函数,说明用户传递是getter,直接把source赋值给getter
        if(typeof source === 'function') getter = source
        else getter = ()=>traverse(source) // 否则调用traverse递归读取
        
        //定义新值和旧值
        let oldValue,newValue
        //使用effect注册副作用函数时,开启lazy选项,将返回值存储到effectFn中以便手动调用
        
        //cleanup用来存储用户注册的过期回调
        let cleanup
        //定义 onInvalidate函数
        function onInvalidate(fn){
            //过期回调存储到cleanup中
            cleanup = fn
        }
        
        //提取scheduler调度函数为一个独立的job函数
        cosnt job=()=>{
                newValue = effectFn()
                
                //调用回调cb之前,调用过期回调
                if(cleanup) cleanup()
                //将新值和旧值作为回调函数的参数
                cb(newValue,oldValue)
                //更新旧值
                oldValue = newValue
        }
        
        const effectFn =  effect(()=>{
            ()=>getter()
        },{
            lazy:true
            scheduler:job
        })
        
        if(options.immediate){
            //当immediate为true时,立即执行job
            job()
        }else{
        //手动调用副作用函数,拿到旧值
        oldValue = efffectFn        
        }

    }