Vue2 中的ref
首先我们回顾一下 Vue2 中的 ref。
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
javascript
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>
<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
其实也就是给元素或者是子组件打上标记,然后通过在父组件中 通过 this.refs.xxx拿到这个 DOM元素或者是组件实例,进而操作 DOM 或者访问组件实例。
在官方文档上声明,ref是一个特殊的属性,$refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
Vue3 setup 中直接定义的数据为什么改变之后,视图不同步
如果我按照上一节 setup 中的模式 ,直接创建变量且赋基础值,然后试图改变变量的值,我们看看会发生什么
javascript
<template>
<p>{{ name }}</p>
<button @click="changeName">改变名称</button>
</template>
<script>
export default {
name: "App",
setup() {
let name = "al";
function changeName() {
name = '汤圆仔'
}
return {
changeName,
name,
};
},
};
</script>
但是,在我点击按钮之后,我发现页面上的名称并没有发生改变。造成的原因可能有两个,
第一个可能是,数据改变了,但是视图没更新。第二种则是,数据本身就没改变所以不更新视图。
验证:改变数据之后,直接打印修改后的变量
从这里可以看到,我们修改的数据其实时发生了改变的,但是页面上却没有更新,这在 Vue2 中是不会存在的,因为 Vue2 中定义在 data 中的数据时响应式的,所以我们可以得出下面这个结论:那就是 直接在 setup 中声明的变量,不是响应式的。
为了解决这一个问题,Vue3 推出了 ref 函数 用来将 setup 中定义的变量转化为响应式。
Vue3 中的 ref 函数
首先,Vue3 中的 ref 是一个函数,区别于Vue2 中的ref是一个特殊属性。作用是将数据转化为响应式。在使用时,需要引入,然后将要转化的数据传入到 ref() 函数中。
javascript
<template>
<p>{{ name }}</p>
<button @click="changeName">改变名称</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: "App",
setup() {
let name = ref("al");
function changeName() {
name = '汤圆仔'
console.log(name,'name');
}
return {
changeName,
name,
};
},
};
</script>
到了这一步,我们再次点击按钮 ,发现还是同样的结果,数据改变了,但是视图没刷新。不是说好的 通过 ref() 函数就能将数据转化为响应式的么,我这都转化了,为啥还是这样?
ref() 函数处理基本数据类型
对于上面的 基本数据类型 name,我们可以在改变值之前,先看看这个 name 变量,被 ref() 函数处理成了什么样子。
可以看到 ref()函数将基本类型数据转化为了一个 ref引用对象(RefImpl对象),同时我们展开对象查看内部属性可以发现存在以下属性。
dep对象暂时猜测和Vue2一致,用来收集分发依赖的。四个带有_前缀的属性一般也是用来给 Vue底层源码使用的,所以这个value一看就是给我们开发者的。
而且我们还可以发现,鼠标放到value属性值的省略号上之后,提示是通过gettrt 方法获得的值,这和Vue2获取响应式数据一致。
然后打开 Prototype 属性,可以看到,针对于这个基础类型的值的 getter 和 setter方法,以及真正的value值。将方法以及初始值放在原型上是利用原型的作用,避免外层数据繁杂。然后将真正的value暴露给外层是为了方便开发者使用。
到这里我们就可以大胆的推论得出:ref() 函数在处理基础类型的值时,通过将其转化为了 RefImpl 引用实例对象后,还是通过 getter 和 setter 来实现响应式的。
得到结论之后还不够,我们要知道怎么去修改或访问通过ref()函数转化的基础数据啊。
在上面通过 name = '汤圆仔' 直接修改属性值被证明是已经行不通了的。因为这样修改之后,相当于是把这个响应式属性直接变成了一个基础类型的值,从而失去了响应式功能。
而看着 RefImpl 引用实例对象中的属性,我们能理解并使用的也就只有 value 属性了。所以当我们修改数据的时候,通过 name.value = '汤圆仔',就能在保证响应式的前提下修改数据了。
javascript
function changeName() {
name.value = '汤圆仔'
console.log(name,'name');
}
同理,在模板中使用数据的时候,我们好像也应该通过 插值语法的形式 {{ name.value }} 去使用,但是在你真这么做之后,你会发现,页面渲染其实错误了。
html
<p>姓名:{{ name.value }}</p>
这其实是因为,Vue3底层设计考虑到了这一问题,在模板中使用变量,Vue3判断当前为插值语法,且使用的是通过 ref() 函数进行转化过后的响应式数据后,会自动解包,自动读取value值,而不需要开发者手动 xxx.value 去获取属性值。所以,我们还是像以前一样,通过插值语法直接使用该属性即可。
ref() 函数处理引用类型值
上面说的是ref()函数对于基本数据类型的值的处理。但是如果我的数据比较多,那我分别调用ref比较麻烦,所以 ref() 函数也支持传入对象形式的数据
javascript
<template>
<p>姓名:{{ userInfo.name }}</p>
<p>姓名:{{ userInfo.age }}</p>
<p>姓名:{{ userInfo.work }}</p>
<button @click="changeName">改变名称</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: "App",
setup() {
let userInfo = ref({
name: 'al',
age: 29,
work:'前端'
});
function changeName() {
console.log(userInfo,'userInfo');
}
return {
changeName,
userInfo,
};
},
};
</script>
点击按钮,控制台打印出当前通过ref()函数转化后的 userInfo 属性,我们能发现返回的还是一个RefImpl引用实例对象,而且 value还是通过 getter转化为响应式的。此时我们不点开 value的值,按照基本类型的处理方式推测一下,此时的value应该是一个对象。
于此同时我们也应该想到一个问题,那就是在Vue2 中,实现了对象的深层响应,那么在Vue3中不可能丢掉这个功能,所以我们可以推断此时 ref()函数对于引用类型的值也做了深层响应式,也应该是针对于引用类型中每个属性都应该返回一个refImpl引用对象实例,以此来保障数据完全深层响应。
那么我们可以推断出,当我们在改变unseInfo内部 name 属性的时候,我们也应该通过 name.value 去修改,也就是说当我们需要修改对象内部属性时,我们需要这样做:先通过 userInfo.value 拿到转化为响应式的 userInfo 对象,然后修改name时,也需要拿到 name 的value去修改
javascript
function changeName() {
console.log(userInfo);
userInfo.value.name.value = '汤圆仔'
}
但是这时候我们发现页面报错了,且提示信息为:不能在一个字符串 al 中读取 value 属性。也就是说 userInfo.value.name 之后是取不到 value属性的。这么搞就有点混乱了啊,那我到底是加还是不加呢?
为了解决这个问题,我们看看 userInfo.value 到底返回的是个啥玩意。点开 value属性之后我们发现 value 属性并不是一个 refImpl引用实例对象,而是一个 Proxy 代理对象。而且这个代理对象上的每个属性只有键值对对应,并没有所谓的 value 属性,所以这个时候我们就需要明白一个问题:Vue3 对于基础类型和引用类型转化为响应式,用的是不同的底层逻辑。
针对于基本类型的数据,Vue3走的还是和Vue2一样的 defineProperty 的getter、setter的数据劫持的方式实现的响应式。
而针对于引用类型的数据,Vue3 走的则是通过Proxy代理的方式实现的响应式( 下一节仔细讲讲怎么通过Proxy实现引用类型的响应式转化 )
搞明白了上面这个value属性值的问题,当我们需要改变引用类型中的数据时,我们就可以这样做
javascript
function changeName() {
userInfo.value.name = '汤圆仔'
}
总结
ref的作用:定义一个响应式的数据
ref的语法:let xxx = ref('initvalue')
- 创建一个 包含响应式数据的引用对象
- 在js中操作数据时,需要使用 xxx.value 来修改
- 在模板中使用数据时,不需要通过 .value来读取,因为 Vue底层会自动解包
ref的参数:可以是基本类型,也可以是引用类型,但是对于这两种数据,响应式处理是完全不同的两套逻辑
- 基本类型数据:依然是通过 Object.defineProperty() 中的 get 与 set 进行数据劫持完成响应式
- 引用类型数据:Vue3 内部求助了一个新函数 -- reactive 函数,通过ES6自带的 Proxy 方法完成了响应式( 参考下一节 -- reactive 函数实现引用类型响应式 )