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
}
}