watch值的就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数,例如:
watch(obj, () => {
console.log('数据变了')
})
// 修改响应数据的值,会导致回调函数执行
obj.foo++
假设obj是一个响应数据,使用watch函数观测它,并传递一个回调函数,当修改响应式数据的值时,会触发该回调函数执行。
watch的实现本质上就是利用effect以及options.scheduler选项,如下代码所示:
effect(()=>{
console.log(obj.foo)
},{
scheduler(){
// 当obj.foo的值发生变化时,会执行scheduler调度函数
}
})
可以看到如果副作用函数存在scheduler选项,当响应式数据发生变化时,会触发scheduler调度函数重新执行,而非直接触发副作用函数执行。这样来看,其实scheduler调度函数就是一个回调函数,而watch正是利用了这个特点,下面是watch的简单实现:
// watch函数接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb){
effect(
// 触发读取操作,从而建立联系
()=>source.foo,
{
scheduler(){
// 当数据变化时,调用回调函数cb
cb()
}
})
}
用如下代码使用watch
const data = {foo:1}
const obj = new Proxy(data, /*...*/)
watch(obj,()=>{
console.log("数据变化了")
})
obj.foo++
上面的代码可以正常运行,但是在watch中,硬编码了对source的读取操作,为了让watch函数具有通用性,需要一个封装一个通用的读取操作
function watch(source, cb){
effect(
// 调用traverse递归地读取
()=>traverse(source),
{
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读取对象的每个值,并递归地调用traverse进行处理
for(const k in value){
traverse(value[k],seen)
}
return value
}
这样就能读取一个对象上的任意属性
watch函数除了可以观测响应式数据,还可以接收一个getter函数:
watch(
// getter函数
()=>obj.foo,
//回调函数
()=>{
console.log('obj.foo的值变了')
}
)
在getter函数内部,用户可以指定该watch依赖那些响应式数据,只有当这些数据变化时,才会触发回调函数执行,如下面代码:
function watch(source, cb){
//定义getter
let getter
//如果source时函数,说明用户传递的是getter,所以直接吧source赋值给getter
if(typeof source === 'function'){
getter = source
}else{
// 否则就按原来的实现调用traverse递归地读取
getter = () => traverse(source)
}
effect(
// 执行getter
()=>getter,
{
scheduler(){
// 当数据变化时,调用回调函数cb
cb()
}
})
}
这里其实还缺少一个非常重要的能力,也就是在回调函数中拿不到旧值和新值。
那么如何获得新值域旧值,这就需要充分利用effect函数的lazy选项,如下代码所示:
function watch(source, cb){
//定义getter
let getter
//如果source时函数,说明用户传递的是getter,所以直接吧source赋值给getter
if(typeof source === 'function'){
getter = source
}else{
// 否则就按原来的实现调用traverse递归地读取
getter = () => traverse(source)
}
//定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
const effectFn = effect(
// 执行getter
()=>getter,
{
lazy:true
scheduler(){
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
//将旧值和新值作为回调函数的参数
cb(newValue, oldValue)
//更新旧值,不然下次会得到错误的旧值
oldValue = newValue
}
})
// 手动调用作用函数,得到旧值
oldValue = effectFn()
}
lazy为true不会立即执行副作用函数