Vue2 中的ref
首先我们回顾一下 Vue2 中的 ref。
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
<!-- `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 中的模式 ,直接创建变量且赋基础值,然后试图改变变量的值,我们看看会发生什么
<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() 函数中。
<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 = '汤圆仔',就能在保证响应式的前提下修改数据了。
function changeName() {
name.value = '汤圆仔'
console.log(name,'name');
}
同理,在模板中使用数据的时候,我们好像也应该通过 插值语法的形式 {{ name.value }} 去使用,但是在你真这么做之后,你会发现,页面渲染其实错误了。
<p>姓名:{{ name.value }}</p>
这其实是因为,Vue3底层设计考虑到了这一问题,在模板中使用变量,Vue3判断当前为插值语法,且使用的是通过 ref() 函数进行转化过后的响应式数据后,会自动解包,自动读取value值,而不需要开发者手动 xxx.value 去获取属性值。所以,我们还是像以前一样,通过插值语法直接使用该属性即可。
ref() 函数处理引用类型值
上面说的是ref()函数对于基本数据类型的值的处理。但是如果我的数据比较多,那我分别调用ref比较麻烦,所以 ref() 函数也支持传入对象形式的数据
<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去修改
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属性值的问题,当我们需要改变引用类型中的数据时,我们就可以这样做
function changeName() {
userInfo.value.name = '汤圆仔'
}
ref() 函数响应式原理
ref() 函数能接收基本数据类型,也能接收引用类型的数据,所以Vue3推荐使用 ref() 函数来实现响应式,但是使用上我们一般还是使用 reactive() 函数较多,后续会讲到。
而且我们需要注意的是,ref()函数与 reactive()函数对于响应式的转化其实时不同的。
- 在调用 ref() 函数时,会先调用 createRef() 函数。
export function ref(value) { return createRef(value, false); }
- 然后在 createRef() 函数中 判断当前接收的参数是否为 ref 对象,如果是,则直接返回该响应式数据,避免重复转化,如果不是,则调用 new RefImpl() 构造函数生成 RefImpl 引用对象
// 接收两个参数, // 第一个参数 rawValue 是需要转化为 ref 的原始值, // 第二个参数 shallow 是一个布尔值,标识是否是浅层响应,如果为 true,则只处理表面层次的响应式,而不会递归处理嵌套对象 function createRef(rawValue, shallow) { // 判断需要转化的数据是否已经是 ref 对象,如果是,则直接返回该数据,避免重复转化 if (isRef(rawValue)) { return rawValue; } // 如果不是 ref 对象,则调用 RefImpl 构造函数生成新的 RefImpl 引用对象 // 同时传递 rawValue 和 shallow 来初始化响应式数据以及确定相应深度 return new RefImpl(rawValue, shallow); }
-
RefImpl 类:是 ref 对象的实际实现。主要包括
-
存储原始值以及响应值:_rawValue 存储原始值,value存储响应值
-
响应式处理:通过 shallow 来决定是否进行深层响应式数据处理
-
依赖收集与分发:在 get 函数中通过
trackRefValue
函数来收集依赖。在set函数中通过triggerRefValue
函数通知依赖更新。 -
RefImpl 类解析
class RefImpl { private _value: any; // 用来存储响应值 private _rawValue: any; // 用来存储原始值 public dep?: Dep = undefined; // 用来收集分发依赖 public readonly __v_isRef = true; //是否只读,暂不考虑 // 接收 new RefImpl() 传递过来的 rawValue 和 shallow constructor(value, public readonly __v_isShallow: boolean) { // 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应 this._rawValue = __v_isShallow ? value : toRaw(value); // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应 this._value = __v_isShallow ? value : reactive(value); } get value() { // 收集依赖 trackRefValue(this); // 返回响应式数据 return this._value; } set value(newVal) { // 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖 if (hasChanged(toRaw(newVal), this._rawValue)) { //判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应 this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal); // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应 this._value = this.__v_isShallow ? newVal : reactive(newVal); // 分发依赖,通知更新 triggerRefValue(this); } } }
-
trackRefValue() 函数:用来收集依赖
// 接收参数 ref ,也就是当前 refImpl 引用实例对象 function trackRefValue(ref) { // 判断当前是否处于依赖收集状态,在 Vue2.x 中,相当于 window.target 。一般用来判断当前是否有活跃的响应式副作用正在运行 if (isTracking()) { // ref.dep 是 RefImpl 实例对象上的一个属性,相当于 Vue2.x中的 Dep 类,用来收集或分发依赖 // 判断 ref.dep 是否存在。若存在则直接使用,若不存在,则通过 createDep 函数创建一个新的依赖集合并赋值给 ref.dep ,然后使用 // 将当前活跃的副作用(effect)添加到 ref.dep 中,以便在将来 ref 值变化时能够触发这些副作用。 trackEffects(ref.dep || (ref.dep = createDep())); } }
-
triggerRefValue()函数:用来分发依赖
function triggerRefValue(ref) { // ref.dep:依赖集合。如果存在依赖集合,则继续进行触发操作。 if (ref.dep) { // 遍历并执行 ref.dep 中的所有副作用(effect),以响应 ref 值的变化。这个函数会通知所有依赖于 ref 值的副作用重新运行。类似于 Vue2.x中的 nofiny() triggerEffects(ref.dep); } }
-
如果需要深层响应( 也就是说,ref()函数接收的参数是一个引用对象),那就需要依赖 reactive() 函数来实现,reactive() 函数的具体原理我们在下一节会详细讲到。
-
总结
ref的作用:定义一个响应式的数据
ref的语法:let xxx = ref('initvalue')
- 创建一个 包含响应式数据的引用对象
- 在js中操作数据时,需要使用 xxx.value 来修改
- 在模板中使用数据时,不需要通过 .value来读取,因为 Vue底层会自动解包
ref的参数:可以是基本类型,也可以是引用类型,但是对于这两种数据,响应式处理是完全不同的两套逻辑
- 基本类型数据:依然是通过 Object.defineProperty() 中的 get 与 set 进行数据劫持完成响应式
- 引用类型数据:如果需要深层响应,Vue3 内部求助了一个新函数 -- reactive 函数,通过ES6自带的 Proxy 方法实现了响应式,并通过 Reflect 操作源对象内部的数据( 参考下一节 -- reactive 函数实现引用类型响应式 )