【vue设计与实现】原始值的响应式方案 2-响应丢失问题

响应丢失问题

ref除了能用于原始值的响应式方案外,还能用来解决响应丢失问题。
首先来看什么是响应丢失问题
在编写Vue.js组件时,通常要把数据暴露到模板中使用,例如:

export default{
	setup(){
		// 响应式数据
		const obj = reactive({foo:1, bar:2})
		// 将数据暴露到模板中
		return {
			...obj
		}
	}
}

然后在模板中访问从setup中暴露出来的数据:

<template>
	<p>{{foo}} / {{bar}}</p>
</template>

但是这么做会导致响应丢失,也就是,修改响应式数据的值时,不会触发重新渲染

export default{
	setup(){
		// 响应式数据
		const obj = reactive({foo:1, bar:2})
		// 1s后修改响应式数据的值,不会触发重新渲染
		setTimeout(()=>{
			obj.foo = 100
		}, 1000)
		return {
			...obj
		}
	}
}

这个问题其实是展开运算符(…)导致的,下面这段代码,

return{
	...obj
}

等同于

return{
	foo:1,
	bar:2
}

这里相当于返回了一个普通对象,其没有任何响应式能力。这样把一个普通对象暴露到模板中使用,不会在渲染函数和响应式数据之间建立响应联系的。
这里可以用另一种方式来描述响应丢失问题:

// obj是响应式数据
const obj = reactive({foo:1, bar:2})

// 将响应式数据展开到一个新的对象newObj
const newObj = {
	... obj
}

effect(()=>{
	// 在副作用函数内通过新的对象newObj读取foo属性值
	console.log(newObj.foo)
})

//这里修改obj.foo会不触发响应
obj.foo = 100

那么如何解决这个问题,换句话说,如何在副作用函数内,通过普通对象newObj来访问属性值,也能够建立响应联系,代码如下:

// obj是响应式数据
const obj = reactive({foo:1, bar:2})

//newObj对象下具有与obj对象同名的属性,并且每个属性值都是一个对象
//该对象具有一个访问器属性value,当读取value的值是,其实读取的是obj对象下响应的属性值
const newObj = {
	foo: {
		get value(){
			return obj.foo
		}
	},
	bar: {
		get value(){
			return obj.bar
		}
	}
}

effect(()=>{
	// 在副作用函数内通过新的对象newObj读取foo属性值
	console.log(newObj.foo)
})

//这时能够触发响应了
obj.foo = 100

这样在副作用函数内读取newObj.foo时,等价于间接读取了obj.foo的值,这样响应式数据自然能够与副作用函数建立响应联系。
继续观察会发现foo和bar的结构非常相似,我们可以将这种结构抽象出来并封装为函数,如下面代码所示:

function toRef(obj, key){
	const wrapper = {
		get value(){
			return obj[key]
		}
	}
	return wrapper
}

有了toRef函数,就可以重新实现newObj对象

const newObj = {
	foo: toRef(obj,'foo'),
	bar: toRef(obj,'bar')
}

如果响应式数据obj的键非常多,那么可以封装toRefs函数来批量完成转换:

function toRefs(obj){
	const ret = {}
	// 使用for...in循环遍历对象
	for(const key in obj){
		ret[key] = toRef(obj,key)
	}
	return ret
}

这样只需要一步就能完成一个对象的转换:

const newObj = {...toRefs(obj)}

可以使用如下代码进行测试:

const obj = reactive({foo:1, bar:2})

const newObj = {...toRefs(obj)} 
console.log(newObj.foo.value)  //1
console.log(newObj.bar.value) //2

现在响应丢失就被彻底解决了,解决问题的思路就是,将响应式数据转换成类似于ref结构的数据。为了概念上的统一,要将通过toRef和toRefs转换后的结果视为真正的ref数据,为此要给toRef增加一段代码:

function toRef(obj, key){
	const wrapper = {
		get value(){
			return obj[key]
		}
	}
	Object.defineProperty(wrapper, '_v_isRef',{
		value: true
	})
	return wrapper
}

但这样上面实现的toRef函数还有缺陷,这样的toRef函数创建的ref是只读的,因为其返回的wrapper对象的value属性只有getter,没有setter,为了功能完整性,还要加上setter,实现如下:

function toRef(obj, key){
	const wrapper = {
		get value(){
			return obj[key]
		}
		//允许设置值
		set value(val){
			obj[key] = val
		}
	}
	Object.defineProperty(wrapper, '_v_isRef',{
		value: true
	})
	return wrapper
}

知识扩展:
访问器属性,可以参考下面的博文
https://blog.csdn.net/qq_43525183/article/details/122460235

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值