不知不觉中,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渲染器原理,大家期待不期待。