前文我们讲在setup
里创建数据直接let name = 'yujiabao'
,然后在return
出去就可以使用了的时候,说了这样创建的数据不是响应式的,不会再视图上及时更新,现在我们就来讲讲VUE3如何创建响应式数据。
插一嘴VUE2创建响应式数据是直接放在data
里就好了,底层会给数据做数据代理和数据劫持,所以直接就是响应式数据。
VUE3创建响应式数据的方式有两种,一种是通过ref
去创建响应式数据,还有一种是通过reactive
去创建响应式数据。
ref创建基本类型的响应式数据
首先我们要先引入这个ref
,可以直接在setup
语法糖里引入。
html
<script setup lang="ts">
import { ref } from "vue";
</script>
引入ref
以后,我们就可以把想变成响应式数据的变量用ref
包裹起来,所以其实ref
是一个函数,我们传一个参数过去,就可以返回一个响应式数据,我们可以打印看一下。
html
<script setup lang="ts">
import { ref } from "vue";
let name = ref("yujiabao");
let age = ref("18");
let gender = "男";
console.log(name);
console.log(age);
console.log(gender);
</script>

可以看到ref
函数会返回一个RefImpl
对象,这个对象里有很多变量,其中带下划线_
的我们不用管,里面有一个变量叫value
,这个是我们要在意的,这个value
就是我们要的响应式数据。
那这个时候是不是在使用的时候要在后面加上.value
了呢?比如使用name
变量,既然已经是一个对象了,我们在模板使用的时候是不是要name.value
呢?其实不用,因为模板会自动给我们加上这个.value
,我们就直接使用name
就好了。
html
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<div>{{ gender }}</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
let name = ref("yujiabao");
let age = ref("18");
let gender = "男";
</script>

但是我们在js
或者ts
里操作这个变量的时候必须要.value
。
html
<template>
<div>{{ name }}</div>
<div>{{ age }}</div>
<div>{{ gender }}</div>
<button @click="changeName">修改姓名</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
let name = ref("yujiabao");
let age = ref("18");
let gender = "男";
function changeName() {
name.value = "于家宝";
}
</script>
如果不加.value
会提示报错,因为此时name
已经不是一个字符串而是一个对象了,在模板可以直接使用name
是因为模板会自动帮你加上.value
。

reactive创建复杂类型的响应式数据
首先和ref
一样,要先引入reactive
。
html
<script setup lang="ts">
import { reactive } from "vue";
</script>
使用方式跟ref
也差不多,用reactive
包裹起来的变量就是响应式变量了。
html
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
console.log(person);
</script>

我们可以看到reactive
函数会返回一个Proxy
对象,对象里面有两个变量,其中Target
里就是我们需要的响应式数据。
同样在模板使用的时候,我们不需要在后面.Target
,因为模板会替我们处理好这些。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
</template>
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
console.log(person);
</script>

但是有一点不一样,我们讲在js
或者ts
里操作ref
返回的响应式数据的时候是需要加上.value
的,那么我们在js
或者ts
里操作reactive
返回的响应式数据的时候是否也需要加上.Target
呢?其实是不需要的,直接操作就好了。
html
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
function change() {
person.name = "月亮";
}
</script>
ref创建复杂类型的响应式数据
前面我们讲了ref
可以创建基本类型的响应式数据,其实ref
也可以创建复杂类型的响应式数据,插句嘴,reactive
不能创建基本类型的响应式数据。
html
<script setup lang="ts">
import { ref } from "vue";
let person = ref({ name: "yueliang", age: "18" });
console.log(person);
</script>

可以看到被ref
包裹起来的复杂数据同样也是一个RefImpl
对象,对象里面也有一个value
变量,但是这个value
变量可不是想象中的那个{name: 'yueliang',age: '18'}
,而是一个Proxy
对象,这就又回到reactive
的知识点了,里面有一个Target
变量,存放的就是我们需要的响应式数据。
这说明什么,说明ref
在让复杂数据变成响应式的时候,会在底层再进行一次reactive
的调用,所以RefImpl.value
又被reactive
包裹了一次,就是说这个时候person.value
就是reactive({name: 'yueliang',age: '18'})
,这就是ref
创建复杂类型响应式数据的原理。
这个时候要注意一点,那此时在js
或者ts
里操作这个person
的时候,还能跟reactive
一样吗?是不能一样的。
html
<script setup lang="ts">
import { ref } from "vue";
let person = ref({ name: "yueliang", age: "18" });
function change() {
person.name = "月亮";
}
</script>
这时候是有报错的:

此时这个person
就是一个RefImpl
对象,没有name
属性,根据前面讲的,在js
或者ts
里操作是需要加上.value
的,此时peoson.value
才是我们想要的{name: 'yueliang',age: '18'}
,所以操作的时候应该这么写。
html
<script setup lang="ts">
import { ref } from "vue";
let person = ref({ name: "yueliang", age: "18" });
function change() {
person.value.name = "月亮";
}
</script>
ref对比reactive
前面我们讲了ref
和reactive
都可以定义响应式数据,但是它们两者之间有什么区别呢,我们这里也要讲一下。
ref既能定义基本数据类型,也可以定义复杂数据类型,而reactive只能定义复杂数据类型。
这一点前面已经说了,在这里就不再赘述了。
ref定义的数据在js或者ts中使用的时候需要加.value,而reactive定义的不需要。
这一点前面也说了,不赘述了。
reactive定义的复杂类型响应式数据被重新定义以后会失去响应式
我们可以对reactive
定义的复杂数据类型进行属性上的修改,比如修改person
对象的name
属性或者age
属性,这是可以的,修改完person
还是响应式数据,但是不能直接修改整个person
,这相当于对person
对象进行重新定义了,这样修改完以后person
数据就不是响应式的了。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
<button @click="changeName">changeName</button>
<button @click="changeAge">changeAge</button>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
function changeName() {
person.name = "月亮";
console.log(person, "修改name属性,还有响应式");
}
function changeAge() {
person.age = "19";
console.log(person, "修改age属性,还有响应式");
}
function change() {
person = {
name: "月亮",
age: "19",
};
console.log(person, "修改整个person对象,相当于重新定义person对象,失去响应式");
}
</script>
比如上述代码,其中changeName
方法和changeAge
方法都是能够生效的,视图也会更新我们想要的结果,但是change
方法点击以后视图就会没有反应,其实数据是已经改变了,但是因为修改了整个person
对象,导致失去了响应式,所以视图没有更新,我们打印在后台就可以很清楚的看到。

其实这里就可以看到,person
本身不是普通对象,而是一个Proxy
对象,这个才是具备响应式的,而我们重新给它定义成一个普通对象,这才会让它失去响应式导致视图不会更新。
那有人就会想了,既然普通对象不行,那重新定义成一个reactive
包裹的对象可以吗,我们可以试一试。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
function change() {
person = reactive({
name: "月亮",
age: "19",
});
console.log(person, "用reactive重新定义person对象");
}
</script>

会发现其实还是不行,那是因为最开始的person
是被reactive
定义的响应式对象,这也是在页面上展示的,但是person
再被reactive
重新定义,那就是一个新的person
对象了,就不是页面上的那个person
对象了,所以页面上的那个不会改变,这听起来很绕口,但是这么理解就好了。
那既然不能重新定义这个person
对象,如果遇到了很多属性都需要去修改,难不成不重新定义,去一个一个修改吗?其实也不用一个一个去修改属性,我们可以使用Object.assign
方法去批量修改属性。
Object.assign(obj, obj1, obj2...)
- 接收多个参数,会把
obj1(包括)
以后对象的属性都加到obj
里面去,如果属性名相同就会直接覆盖。- 返回值:就是这个obj。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
<button @click="changeName">changeName</button>
<button @click="changeAge">changeAge</button>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: "18" });
function changeName() {
person.name = "月亮";
console.log(person, "修改name属性,还有响应式");
}
function changeAge() {
person.age = "19";
console.log(person, "修改age属性,还有响应式");
}
function change() {
Object.assign(person, {
name: "月亮",
age: "18",
});
console.log(person, "使用Object.assign方法修改整个person对象");
}
</script>

这样就可以解决了。
ref定义的复杂类型响应式数据被重新定义以后不会失去响应式
前面说了reactive
定义的复杂数据类型不能重新定义,重新定义会失去响应式,如果遇到需要修改多个属性的时候就使用Object.assign
方法,但是ref
定义的复杂数据类型重新定义是不会失去响应式的。不过这也要加上.value
,其实严格来说这也不算是重新定义。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
let person = ref({ name: "yueliang", age: "18" });
function change() {
person.value = {
name: "月亮",
age: "19",
};
console.log(person);
}
</script>

如果真的说是重新定义,就应该是下面这样,不加上这个.value
。

但是这样写都是有错的,因为此时person
是一个RefImpl
对象,不能这么赋值。

就算是用ref
重新定义,也跟前面讲reactive
重新定义的时候一样,是没有响应式的,因为页面上展示的是原来的person
对象,现在person
被重新定义了,就不是页面上展示的那个了,所以页面上的数据也是不会改变。
html
<template>
<div>{{ person.name }}今年{{ person.age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
let person = ref({
name: "yueliang",
age: "18",
});
function change() {
person = ref({
name: "月亮",
age: "19",
});
console.log(person);
}
</script>

就算是ref
定义的基本数据类型也不能这么修改,是没有效果的。
html
<template>
<div>{{ person }}</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
let person = ref(0);
function change() {
person = ref(9);
console.log(person);
}
</script>

toRefs
我们现在已经对ref
和reactive
去定义响应式数据有一些了解了,那我们在对这个复杂类型的响应式数据进行解构赋值的时候,解构出来的数据还具备响应式吗?
html
<template>
<div>{{ name }}今年{{ age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
import { reactive } from "vue";
let person = reactive({ name: "yueliang", age: 18 });
let { name, age } = person;
console.log(person);
console.log(age);
function change() {
age++;
console.log(age);
}
</script>
会发现展示是没有问题的,但是点击按钮的时候页面上的年龄没有预想中的递增+1
。

而且点击以后其实数据是已经改变的了,显而易见,这解构出来的数据没有响应式,而且前面打印也可以看到,打印出来的就只是一个普通的数据,不是RefImpl
对象或者Proxy
对象。
说白了其实let { name, age } = person
就只是相当于下面这段代码:
js
let name = person.name 'yueliang'
let age = person.age 18
只是简单的读取了person.name
的值而已,看后台打印也可以看出来,就是一个普通的数据。
那怎么才能让解构出来的数据是响应式呢?这就要用到toRefs
了。
html
<template>
<div>{{ name }}今年{{ age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
// 先引入toRefs
import { reactive, toRefs } from "vue";
let person = reactive({ name: "yueliang", age: 18 });
let { name, age } = toRefs(person);
console.log(age);
function change() {
age++;
console.log(age);
}
</script>
这个toRefs
从字面意思都能看出来,就是把某些东西给转换成ref
包裹的,也就是响应式数据,这里就是把解构出来的name
,age
给转换成RefImpl
对象,这样就是响应式的了,其实截图就可以看出来:

这里age
报错了,就是因为此时age
已经是RefImpl
对象了,根据我们前面所讲的,js
或者ts
里操作时需要加上.value
的,所以这里报错。
我们给它加上,然后去试一试看看页面能不能及时更新。
html
<template>
<div>{{ name }}今年{{ age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
// 先引入toRefs
import { reactive, toRefs } from "vue";
let person = reactive({ name: "yueliang", age: 18 });
let { name, age } = toRefs(person);
console.log(age);
function change() {
age.value++;
console.log(age);
}
</script>

可以看到数据是可以更新的了,而且打印出来的是一个ObjectRefImpl
对象,其实也就是一个RefImpl
对象,是一个响应式数据。
其实我们也可以去打印一下这个toRefs(person)
,看看到底是个什么东西。

可以看到toRefs(person)
就是一个普通的对象,但是对象里面的每一个属性都是ObjectRefImpl
对象,这个对象里也有一个value
属性,是不是证明了前面所说的ObjectRefImpl
对象也就是RefImlpl
对象。那这么一来,let { name, age } = person
就可以看作下面这些代码:
js
let name = name (ObjectRefImpl)
let age = age (ObjectRefImpl)
总的来说就是toRefs
可以把所包裹的对象中的每一个属性都被ref
包裹转换成RefImpl
对象,所以这样解构出来的数据才具有响应式。
还有一个注意点:那此时的age
和person.age
是否还有关联呢?
答案是有的,我们去试一下:
html
<template>
<div>{{ name }}今年{{ age }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
// 先引入toRefs
import { reactive, toRefs } from "vue";
let person = reactive({ name: "yueliang", age: 18 });
let { name, age } = toRefs(person);
function change() {
age.value++;
console.log(age.value, person.age);
}
</script>

可以看到age
和person.age
是有关联的,就相当于它俩的指针指向的是同一个内存。
toRef
这个跟toRefs
作用差不多,toRefs
是把包裹对象的所有属性都用ref
包裹一下,这个是指定某个属性去包裹。
html
<template>
<div>{{ name }}今年{{ nl }}岁</div>
<button @click="change">change</button>
</template>
<script setup lang="ts">
// 先引入toRefs
import { reactive, toRef } from "vue";
let person = reactive({ name: "yueliang", age: 18 });
let name = toRef(person, "name");
let nl = toRef(person, "age");
function change() {
nl.value++;
console.log(nl.value, person.age);
}
</script>

效果都是一样的,只是toRefs
是把包裹对象的全部属性都用ref
包裹起来变成响应式的,toRef
是把包裹对象的某个属性用ref
包裹起来变成响应式的。