Vue3.0源码系列(八):响应式原理(watch的实现原理)

不知不觉中,Vue3.0的响应式原理来到了最后一个重要的api,那就是watch的源码实现原理。相信大家在项目中每时每刻都在使用watch,在使用的时候,有没有想过其内部源码的实现逻辑那。如果你想要探究其中的奥秘,那么今天就跟着我走进watch的源码世界一探究竟。

watch:所谓watch,其本质就是一个响应式数据,当数据发生变化时候,去执行相应的回调函数。

watch(obj,()=>{
    console.log(123)
})
//当响应式数据发生变化的时候,会重新执行watch中的回调函数。
obj++ 

实际上,obj就是一个响应式数据,使用watch来动态的观测。当数据发生变化时,执行了对应的回调函数。watch的实质其实就是利用了effect和scheduler。

effect(()=>{
    consoole.log(obj.zzq)
},{
    scheduler() {
        //当响应式数据obj变化时候,会执行对应的调度器scheduler
    }
})



function watch(souce,callBack) {
   effect(()=>souce.zzq,{
    scheduler() {
        //当响应式数据obj变化时候,会执行对应的调度器scheduler
         callBack()
    }
   }) 
} 

这时候,我们就能在开发中应用watch函数来实现功能,例如:

const data = {zzq:521}
const reactiveData = new Proxy(data,{})
watch(reactiveData,()=>{
   console.log('执行数据变化后的回调')
})
reactiveData.zzq-- 

可以看到上面简单实现的watch,我们代理了数据data,使其具有响应式。通过watch监听reactiveData数据,当数据变化的时候,这时候就会执行会回调scheduler里面的 callBack()回调,进而触发watch的回调函数执行,输出‘执行数据变化后的回调’,这样就实现了简单的watch。但是仔细看,我们就能发现,我们只是单纯的对data重的 reactiveData.zzq进行了监听,但是watch监听是针对于整个对象当中的所有属性,任意属性值变化时,都会触发回调。所以需要进行简单的封装。

function watch(souce,callBack) {
    //我们对传入的对象souce进行循环遍历,把所有属性进行监听
   effect(()=> traverse(souce),{
    scheduler() {
        //当响应式数据变化时候,会执行回调函数callBack()
         callBack()
    }
   }) 
}

function traverse(target,seen = new Set()) {
    //如果要读取的数据是原始值,不是对象。或者已经被读取过了,就return掉
    if(typeof target !== 'object' || typeof ==null || seen.has(target))return
    //用Set数据结构存储,防止重复循环引用。
    seen.add(target)
    //假设是一个对象,循环调用traverse
    for(const key in target) {
        traverse(target[key],seen)
    }
    return target
} 

这样,我们就封装了一个traverse函数,对watch监听的数据进行循环监听。这样就可以读取对象上的任意属性,当每一个属性发生变化时候,都能够触发watch的回调函数了。

watch不仅可以观测响应式数据,还可以接受getter函数,在其内部,用户可以指定watch依赖那些数据响应式数据。,只有这些数据变化时,才会发生回调。下面我们来兼容这些功能。

watch(()=>obj.zzq,()=>{
    console.log('obj.zzq数据发生了变化')
})

function watch(souce,callBack) {
    let getter
    if(typeof souce === 'function') {
       getter =  souce
    }else {
       getter = ()=> traverse(souce)
    }
    //我们对传入的对象souce进行循环遍历,把所有属性进行监听
   effect(()=>getter(),{
    scheduler() {
        //当响应式数据变化时候,会执行回调函数callBack()
         callBack()
    }
   }) 
} 

我们注意到了,当我们使用watch时候,一般都是用新值和旧值的对比,现在这个watch中还不具备这个能力,下面我们就来一起实现一下newVal和oldVal。

function watch(souce,callBack) {
    let getter
    if(typeof souce === 'function') {
       getter =  souce
    }else {
       getter = ()=> traverse(souce)
    }
    let newVal,oldVal;
    //我们对传入的对象souce进行循环遍历,把所有属性进行监听
   const effectFn = effect(()=>getter(),{
    lazy:true,//lazy 是懒执行effect
    scheduler() {
        //调用effectFn,得到新的newVal
         newVal =  effectFn()
        //当响应式数据变化时候,会执行回调函数callBack()
         callBack(newVal,oldVal)
         //新值替换旧值,当作旧值。
         oldVal = newVal
    }
   }) 
   //我们自己触发点一次effect调用,它肯定优先effect的执行。因为只有当响应式数据变化时候,才会执行effectFn。这是我们手动执行,得到一个初试值oldVal。
  oldVal =  effectFn()
   
} 

这样,我们就实现了这个比较重要的功能,把watch监听的数据newVal和oldVal都返回回来。上面代码重要实现,我们利用effect的lazy属性实现了一个effect的副作用懒执行。手动调用effectFn函数得到了一个旧值,当我们响应式数据发生变化时候,执行scheduler,进而执行effectFn函数,得到一个新值,这是我们把新值和旧值,通过回到函数callBack返回。这样就完成了这个功能。

watch中还有一个非常重要的功能,就是立即执行watch,什么叫做立即执行那?默认情况下,watch的回调只在监听的数据发生比变化时候,进行执行。但是立即执行,就是数据没有变化,初始化时候,先进行一下回到函数执行。在watch中我们通过immediate为true来设置立即执行回调。相信,开发中我们经常在代码中用到。

 watch(()=>obj.zzq,()=>{
    console.log('obj.zzq数据发生了变化')
 },{
    immediate:true
 }) 

这个功能实际更上面的实现没有什么区别,只是需要对immediate出现时候做兼容。即在初始化时候和数据变化时候都执行相应的scheduler。

function watch(souce,callBack,options = {}) {
        let getter
        if(typeof souce === 'function') {
           getter =  souce
        }else {
           getter = ()=> traverse(souce)
        }
        let newVal,oldVal;
        let work = ()=>{
            //调用effectFn,得到新的newVal
             newVal =  effectFn()
            //当响应式数据变化时候,会执行回调函数callBack()
             callBack(newVal,oldVal)
             //新值替换旧值,当作旧值。
             oldVal = newVal
        }
        //我们对传入的对象souce进行循环遍历,把所有属性进行监听
       const effectFn = effect(()=>getter(),{
        lazy:true,//lazy 是懒执行effect
        scheduler:work
       }) 
       if(options.immediate) {
           work()
       }else{
           //我们自己触发点一次effect调用,它肯定优先effect的执行。因为只有当响应式数据变化时候,才会执行effectFn。这是我们手动执行,得到一个初试值oldVal。
          oldVal =  effectFn()
       }
 } 

我们通过判断第三个参数options中是否存在immediate,来动态的执行scheduler。就实现了这个立即执行的功能。但是我们也看到了,使用immediate的时候,我们的oldVal为undefined,这是合理的。因为一开始,oldVal就是没有值。

赶在周日的最后几分钟,完成了我们Vue3源码响应式原理系列的更新,最后一集watch完成。不知道大家有没有一致关注那。下一个系列就是Vue渲染器原理,大家期待不期待。

你可能感兴趣的:(javascript,前端,vue.js)