响应丢失问题
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