先写一个基础的vue3模板
<template>
<div>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
const name = "张三";
const age = 18;
return {
name,
age
};
},
});
</script>
页面正常显示
这时候加一个定时器,修改张三的年龄
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = "张三";
let age = 18;
setInterval(function () {
age++;
console.log("age", age);
}, 1000);
return {
name,
age,
};
},
});
</script>
会发现控制台正常打印,但是数据不是响应式的,也就是数据发生了变化,页面却没有更新。原因是我们仅仅定义了一个string类型和number类型的普通数据,并不是一个vue响应式数据。现在引入vue3中一个函数,ref,官方定义如下:
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
根据vue官方文档的提示,通过ref包裹普通数据,通过.value能够拿到响应式数据,我们可以先打印一下name
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = ref("张三");
let age = ref(18);
console.log("name", name);
return {
name,
age,
};
},
});
</script>
发现’name’是被refImpl类包裹的一个实例对象,这个类可以等会研究研究,先看实例对象上的一个属性:value,值为(…),并且提示’invoke property getter’,意思是调用属性 getter,这说明vue3.0的ref函数是通过vue2.x数据响应式方法Object.defineProperty()作为响应式数据的手段。
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。–MDN
再继续往下看原型链_proto_,打印如下图,其中包含了Object.defineProperty的get和set方法,来读/写value属性完成一个ref数据响应式。_proto_下还存在了一个value属性,值为’张三’,通过数据代理传递到最外层,方便调用。(可以理解成vue2.x中,vm.data与vm._data都能获取data中定义的数据,在template中写value比_data.value更加方便)
数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = ref("张三");
let age = ref(18);
console.log("name", name);
return {
name,
age,
};
},
});
</script>
通过控制台发现name是被refImpl包裹的一个对象
再通过.value获取到name值
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = ref("张三");
let age = ref(18);
setInterval(function () {
age.value++;//被ref包裹的数据,需要通过.value获取值
console.log("age", age);
}, 1000);
return {
name,
age,
};
},
});
</script>
此时数据被ref包裹成为vue响应式数据,页面也可以正常更新。
此时一个响应式的ref数据便完成了,此时再回去看为什么要通过.value的形式获取值,先来看一下refImpl,全称是reference Implement,可以理解成引用对象,来看一下RefImpl关键源码
class RefImpl<T> {
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
-
有一个构造函数constructor,用于构造对象。构造函数接受两个参数:
第一个参数_rawValue,要求是T类型,第二个参数_shallow,默认值为true -
提供了两个方法,get value(){}和set value(){},分别对应私有属性的读写操作,用于供外界操作value
当通过它构建对象时,会给对象的_value属性赋值为_rawValue或者convert(_rawValue)
再看convert源码如下:
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
最终Vue会根据传入的数据是不是对象isObject(val),如果是对象本质调用的是reactive,否则返回原始数据。
现在思考一个问题,通过ref包装的结果,当原始数据改变时会触发界面更新吗?即原始数据和返回的响应式数据是否有关联?
修改一段代码:
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<button @click="add">++</button>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = ref("张三");
let age = 18;
let curAge = ref(age);
console.log("name", name);
const add = () => {
age++;
console.log("age", age);
console.log("curAge", curAge.value);
};
return {
name,
age,
add,
};
},
});
</script>
再打印一下
实例发现,当原始数据发生修改时,并不会影响响应式数据,更不会触发界面UI的更新。
再修改一段代码,让curAge.value++
<template>
<p>个人信息</p>
<p>姓名:{{ name }}</p>
<p>年龄:{{ curAge }}</p>
<button @click="add">++</button>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "Home",
components: {},
setup() {
let name = ref("张三");
let age = 18;
let curAge = ref(age);
console.log("name", name);
const add = () => {
curAge.value++;
console.log("age", age);
console.log("curAge", curAge.value);
};
return {
name,
age,
curAge,
add,
};
},
});
</script>
打印如下
实例发现如果响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据
总结
小结一下:
- ref本质是将一个数据变成一个对象,这个对象具有响应式特点
- ref接受的原始数据返回的对象本质都是RefImpl类的实例
- 无论传入的原始数据时什么类型,当原始数据发生改变时,并不会影响响应数据,更不会触发UI的更新。但当响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据。所以ref中,原始数据和经过ref包装后的响应式数据是无关联的。