【vue设计与实现】响应系统的作用与实现 6-watch的实现原理

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不会立即执行副作用函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值