Vue中watch的实现原理

132 阅读2分钟

所谓watch,其实本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数。例如:

watch(obj,(){
console.log('数据改变了')
})
//修改响应数据的值,会导致回调函数执行
obj.foo++

假设obj是一个响应数据,使用watch函数观测它,并传递一个回调函数,当修改响应数据的值时,会触发该回调函数执行。

watch的实现本质上就是利用了effect以及options.scheduler选项,如下面代码

effect(()=>{
 console.log('obj.foo')
},{
 scheduler(){
 //当obj.foo的值发生变化时,会执行scheduler 调用函数
 }
})

在一个副作用函数中访问响应式数据obj.foo,通过前面的介绍,我们知道这会在副作用函数与响应式数据之间建立联系,当响应式数据变化时,会触发副作用函数重新执行。但有一个例外,即如果副作用函数存在scheduler选项,当响应式数据发生变化时,会触发scheduler调用函数执行。从这个角度来看,其实scheduler调度函数就相当于一个回调函数,而watch的实现就是利用了这个特点。 下面是最简单的watch函数的实现:

//watch函数接受两个参数,source是响应式数据,cd是回调函数
function watch(source,cb){
 effect(
//触发读取操作,从而建立联系
 ()=>source.foo,
 {
 scheduler(){
 //当数据变化时,调用回调函数cb
   cb()
   }
  }
 )
}

我们可以如下所示使用watch函数

coust data = { foo:1 }
coust obj = new Proxy(data,{/*...*/})
watch(obj,()=>{
 console.log('数据变化了')
})
obj.foo++

上面的代码能正常工作,但是我们注意到watch函数的实现中,硬编码了对source.foo的读取操作。换句话说,现在只能观测到obj.foo的变化,为了能让watch函数具有通用性,我们需要封装一个通用的读取操作:

function watch(source,obj){
  effect(
     //调用traverse 递归的读取
     ()=> tarverse递归地读取
     {
     scheduler(){
       //当数据变化时,调用回调函数cb
       cb()
      }
    }
  )
}
function traverse(value,seen = new Set()){
  //如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
  if(typeof value !== 'object' || value === null || seen.has(value))return
  //将读取的数据添加到seen中代表遍历的读取过了,避免循环引用引起死循环
  seen.add(value)
  //暂时不考虑数组等其他结构
  //假设value 就是一个对象 ,使用for ... in 读取对象的每一个数组,并递归的调用tarverse进行处理
  for(const k in value){
    traverse(value[k],seen)
  }
  
  return value
}

如上面的代码,在watch内部的effect中调用traverse函数进行递归的读取操作,代替硬编码的方式,这样就能读取一个对象上的任意属性,从而当人意属性发生变化时都能够触发回调函数执行。