深入源码剖析Vue3 ref
关于ref
,官方的解释是:
接受一个内部值并返回一个响应式且可变的
ref
对象
https://www.vue3js.cn/docs/zh/api/refs-api.html#ref
为了方便理解,下文中将内部值都称为原始数据(orgin
)
简单来说ref
就是:原始数据=>响应式数据 的过程
但有几个问题得搞明白
-
ref
接受的原始数据是什么类型?是原始值还是引用值,还是都行? - 返回的响应式数据本质具体是什么?根据传递的数据类型不同,返回的响应式对象是否不同?
- 响应式数据改变会触发界面更新,那原始数据改变会触发界面更新吗?即原始数据和返回的响应式数据是否有关联
示例代码1:
let origin = 0; //原始数据为原始值
let count = ref(origin);
function add() {
count.value++;
}
示例代码2:
let origin = { val: 0 };//原始数据为对象
let count = ref(origin);
function add() {
count.value.val++;
}
经测试,我们发现,传递的原始数据orgin
可以是原始值也可以是引用值,但是需要注意,如果传递的是原始值,指向原始数据的那个值保存在返回的响应式数据的.value
中,如上count.value;如果传递的一个对象,返回的响应式数据的.value
中对应有指向原始数据的属性,如上count.value.val
为了测试第二个问题,我们将上述示例中的count
打出来,看返回的具体是什么
console.log(count)
console.log(count.constructor)
首先,如果传入的是原始值数据,返回的结果如下:
然后,如果传入的是对象,返回结果如下:
对比发现,不管传递数据类型的数据给ref
,无论是原始值还是引用值,返回的响应式数据对象本质都是由RefImpl
类构造出来的对象。但不同的是里头的value
,一个是原始值,一个是Proxy
对象
到这里,不妨来读一下RefImpl
类的源码
目录:vue-nextpackagesreactivitysrcref.ts
class RefImpl {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, private readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
可以看见RefImpl class
传递了一个泛型类型T
,里头具体包含:
- 有个私有属性
_value
,类型为T
,有个公开只读属性__v_isRef
值为true
- 有两个方法,
get value(){}
和set value(){}
,分别对应私有属性的读写操作,用于供外界操作value
- 有一个构造函数
constructor
,用于构造对象。构造函数接受两个参数:- 第一个参数
_rawValue
,要求是T
类型 - 第二个参数
_shallow
,默认值为true
- 第一个参数
当通过它构建对象时,会给对象的_value
属性赋值为_rawValue
或者convert(_rawValue)
再看convert
源码如下:
const convert = (val: T): T =>
isObject(val) ? reactive(val) : val
通过源码我们发现,最终Vue
会根据传入的数据是不是对象isObject(val)
,如果是对象本质调用的是reactive
,否则返回原始数据
下面再来验证最后一个问题就是:通过ref
包装的结果,当原始数据改变时会触发界面更新吗?即原始数据和返回的响应式数据是否有关联?
示例代码3
let origin = 0; //原始值
let count = ref(origin);
function add() {
origin++
console.log(count.value)
}
示例代码4
let origin = { val: 0 }; //引用值
let count = ref(origin);
function add() {
origin++
console.log(count.value.val)
}
发现,无论传入给ref
的原始数据是原始值还是引用值,当原始数据发生修改时,并不会影响响应式数据,更不会触发界面UI
的更新
实例代码5
let origin = 0;
let count = ref(origin);
function add() {
count.value++
console.log(origin)
}
上述代码,无论count修改多少次,origin一直是0
即如果响应式数据发生改变,对应界面UI
是会自动更新的,注意不影响原始数据
以上就是ref
的使用!
小结一下:
1. ref本质是将一个数据变成一个对象,这个对象具有响应式特点
2. ref接受的原始数据可以是原始值也可以是引用值,返回的对象本质都是RefImpl类的实例
3. 无论传入的原始数据时什么类型,当原始数据发生改变时,并不会影响响应数据,更不会触发UI的更新。但当响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据。所以ref中,原始数据和经过ref包装后的响应式数据是无关联的
注:本文示例代码可在github查阅
https://github.com/jCodeLife/learn-vue3/tree/master/learn-vue3-ref