Vue3中ref和reactive的区别
在Vue3中,我们可以用 ref 和 reactive 来定义响应式数据,但这两者有什么区别,什么情况下用 ref,什么情况下用 reactive 呢?
这篇文章我将给大家详细的讲讲 ref 和 reactive 两者的用法以及使用场景。
ref的使用
作用
ref 的作用是将一个普通的 JavaScript 变量(该变量可以是基本类型的数据,也可以是引用类型的数据)包装成一个响应式的数据。
官方文档的解释是:接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
参数
ref 的参数可以是:基本数据类型、引用数据类型、DOM的ref属性值
基本用法
首先,先看看 ref 函数传入的参数为原始数据类型的情况:
原始数据类型共有7个,分别是:String / Number / Boolean / BigInt / Symbol / Null / Undefined。
<script setup>
import { ref } from 'vue'
const count = ref(0)
const handleCountIncrement = () => {
count.value++
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="handleCountIncrement">count++</button>
</div>
</template>
上面这段代码中
- 首先,我们导入了 vue 提供的 ref 函数
- 然后,使用 ref 函数创建了一个名为 count 的响应式引用,初始值为原始数据类型 0
- 接着,我们定义了一个方法 handleCountIncrement,该方法用于更新 count 的值,当我们点击按钮更新count时,界面的UI也会发生变化。
ref 函数传入的参数为引用数据类型的情况:
<script setup>
import { ref } from 'vue'
const product = ref({ price: 0 })
const changeProductPrice = () => {
product.value.price += 10
}
</script>
<template>
<div class="main">
<p>price: {{ product.price }}</p>
<button @click="changeProductPrice">修改产品价格</button>
</div>
</template>
上面这段代码中
- 首先,我们导入了 vue 提供的 ref 函数
- 然后,使用 ref 函数创建了一个名为 product 的响应式引用,初始值为引用数据类型,是一个对象
- 接着,我们定义了一个方法 changeProductPrice,该方法用于更新 product 对象中 price 的值,当我们点击按钮时,界面的UI也会发生变化。
从上面的代码中,我们可以看到:
ref 函数的参数,我们可以传递原始数据类型的值,也可以传递引用类型的值,但是需要注意的是:
- 如果传递的是原始数据类型的值,那么指向原始数据的那个值保存在返回的响应式数据的 .value 中,例如上面的 count.value;
- 如果传递的一个引用类型的值,例如传个对象,返回的响应式数据的 .value 中对应有指向原始数据的属性,例如上面的 product.value.price。
我们不妨打印一下 count 和 product 这两个响应式数据,看看有什么不一样的地方:
上图中,我们可以看到:不管给 ref 函数传递原始数据类型的值还是引用数据类型的值,返回的都是由 RefImpl 类构造出来的对象,但不同的是对象里面的 value:
- 如果 ref 函数参数传递的是原始数据类型的值,那么 value 是一个原始值
- 如果 ref 函数参数传递的是引用数据类型的值,那么 value 是一个 Proxy 对象
ref的解包
ref在模板中的解包
- 在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
- 如果文本插值表达式( {{ }} )计算的最终值是 ref ,那么也会被自动解包。
在下面的例子中,count 和 person 是顶级属性,但 person. age 不是
<script setup>
import { ref } from 'vue'
const count = ref(0)
const person = {
age: ref(26)
}
</script>
那么下面这种写法会自动解包:
<template>
<div class="main">
<p>count: {{ count + 1 }}</p>
</div>
</template>
但下面这种写法不会自动解包:
<template>
<div class="main">
<p>age: {{ person.age + 1 }}</p>
</div>
</template>
页面渲染的结果是:[object Object]1,因为在计算表达式时 person.age 没有被解包,仍然是一个 ref 对象。
为了解决这个问题,我们可以将 age 解构成为一个顶级属性:
<script setup>
import { ref } from 'vue'
const person = {
age: ref(26)
}
const { age } = person
</script>
<template>
<div class="main">
<p>age: {{ age + 1 }}</p>
</div>
</template>
现在页面就可以渲染出正确的结果了:age: 27
ref在响应式对象中的解包
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。
换句话说,它的行为就像一个普通的属性:
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
</script>
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const state = reactive({
count
})
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
// 原始 ref 现在已经和 state.count 失去联系
console.log(count.value) // 0
</script>
ref在数组合原生集合类型中的解包
与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型(如 Map) 中的元素被访问时,它不会被解包:
<script setup>
import { ref, reactive } from 'vue'
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value) // Vue 3 Guide
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value) // 0
</script>
总结
- ref 函数接受的参数数据类型可以是原始数据类型也可以是引用数据类型。
- 在模板中使用 ref 时,我们不需要加 .value,因为当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,但在js中,访问和更新数据都需要加 .value。
reactive的使用
作用
reactive 的作用是将一个普通的对象转换成响应式对象。它会递归地将对象的所有属性转换为响应式数据。它返回的是一个 Proxy 对象。
参数
reactive 的参数只能是对象或者数组或者像 Map、Set 这样的集合类型。
基本用法
<script setup>
import { reactive } from 'vue'
// 使用 reactive 创建一个包含多个响应式属性的对象
const person = reactive({
name: 'Echo',
age: 25,
})
console.log(person.name); // 读取属性值:'Echo'
person.age = 28; // 修改属性值
console.log(person.age); // 读取修改后的属性值:28
</script>
上面这段代码中
- 首先,我们导入了 vue 提供的 reactive 函数
- 然后,使用 reactive 函数创建了一个名为 person 的响应式对象,对象中有 name 和 age 属性
- 接着,我们读取和修改对象中的属性值。
下面我们在控制台中打印一下 person 对象,看是什么东西:
console.log(person);
可以看到,打印出来的是一个 Proxy 对象,也就是说:reactive 实现响应式就是基于ES6 Proxy 实现的。
那么,Proxy 有几个特点是需要我们注意的:
reactive() 返回的是一个原始对象的 Proxy,它和原始的对象是不相等的。
<script setup>
import { reactive } from 'vue'
const raw = {}
const proxy = reactive(raw)
console.log(proxy === raw) // false
</script>
当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化。
<script setup>
import { reactive } from 'vue'
const obj = {
count: 1
}
const proxy = reactive(obj);
proxy.count++;
console.log(proxy.count); // 2
console.log(obj.count); // 2
</script>
上面这段代码中:
- 我们定义了一个原始对象 obj 和一个代理对象 proxy
- 我们更改代理对象中的 count 值,让它自增1
- 打印原始对象和代理对象的 count,值都是 2
- 说明:代理对象里面的数据发生变化时,对应的原始数据也会发生变化。
我们再看看另外一种情况,将上面代码中的 proxy.count++ 改为 obj.count++。
<script setup>
import { reactive } from 'vue'
const obj = {
count: 1
}
const proxy = reactive(obj);
obj.count++;
console.log(proxy.count); // 2
console.log(obj.count); // 2
</script>
控制台打印的结果也都是 2,说明:当原始对象里面的数据发生改变时,会影响代理对象。
那么问题来了,当原始对象里面的数据发生改变时,会影响代理对象;代理对象里面的数据发生变化时,对应的原始数据也会发生变化,这是必然的,但是我们实际开发中应该操作原始对象还是代码对象?
答案是:代理对象,因为代理对象是响应式的。
官方给出的建议也是如此:只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本。
<script setup>
import { reactive } from 'vue'
const obj = {
count: 1
}
const proxy = reactive(obj);
</script>
<template>
<div class="main">
obj.count:<input type="text" v-model="obj.count">
proxy.count:<input type="text" v-model="proxy.count">
<p>obj.count:{{ obj.count }}</p>
<p>proxy.count:{{ proxy.count }}</p>
</div>
</template>
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
<script setup>
import { reactive } from 'vue'
const raw = {}
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)
console.log(proxy1 === proxy2) // true
console.log(reactive(proxy1) === proxy1) // true
</script>
注意
- 使用 reactive 定义的响应式对象,会深度监听每一层的属性,它会影响到所有嵌套的属性。换句话说:一个响应式对象也将深层地解包任何 ref 属性,同时保持响应性。
<script setup>
import { reactive } from 'vue'
let obj = reactive({
name: 'Echo',
a: {
b: {
c: 1
}
}
})
console.log("obj: ", obj)
console.log("obj.name: ", obj.name)
console.log("obj.a: ", obj.a)
console.log("obj.a.b: ", obj.a.b)
console.log("obj.a.b.c: ",obj.a.b.c)
</script>
控制台打印的结果:
我们可以看到,返回的对象以及其中嵌套的对象都会通过 Proxy 包裹。
若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,我们可以使用 shallowReactive()。
<script setup>
import { shallowReactive } from 'vue'
let obj = shallowReactive({
name: 'Echo',
a: {
b: {
c: 1
}
}
})
console.log("obj: ", obj)
console.log("obj.name: ", obj.name)
console.log("obj.a: ", obj.a)
console.log("obj.a.b: ", obj.a.b)
console.log("obj.a.b.c: ",obj.a.b.c)
</script>
我们可以看到,只有顶层对象会通过 Proxy 包裹,其余嵌套的对象都没有,因此,只有对象自身的属性是响应式的,下层嵌套的属性都不具有响应式。
<script setup>
import { shallowReactive } from 'vue'
let obj = shallowReactive({
name: 'Echo',
a: {
b: {
c: 1
}
}
})
</script>
<template>
<div class="main">
obj.name: <input type="text" v-model="obj.name">
obj.a.b.c: <input type="text" v-model="obj.a.b.c">
<p>obj.name: {{ obj.name }}</p>
<p>obj.a.b.c: {{ obj.a.b.c }}</p>
</div>
</template>
- reactive 的参数只能是对象或者数组或者像 Map、Set 这样的集合类型。如果是原始数据类型,控制台会报警告。
<script setup>
import { reactive } from 'vue'
let count = reactive(0)
</script>
- 当我们将响应式对象的原始类型属性进行解构时,或者将该属性传递给函数时,会丢失响应式。
const state = reactive({ count: 0 })
// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
ref 和 reactive 的区别
经过上面对 ref 和 reactive 的深入了解,我们可以知道它们两者之间的区别了:
- ref 主要用于创建单个的响应式数据。reactive 用于创建包含多个响应式属性的对象。
- 对于基本类型(例如:数字,布尔值)的变量定义,推荐使用 ref,如果需要响应式包装对象或数组,推荐使用 reactive。
- 在模板中使用响应式数据时,无需使用 .value 访问 ref 类型的数据,而是直接使用变量名,而使用 reactive 类型的数据,则直接使用对象属性名。
- reactive 会递归地将对象的所有属性转换为响应式数据。
- ref 返回一个由 RefImpl 类构造出来的对象,而 reactive 返回一个原始对象的响应式代理 Proxy。
还有一种区别,使用 watch 侦听 ref 和 reactive 的方式是不同的,下面详细讲一下有什么不一样的地方。
1. 使用 watch 侦听 ref 定义的响应式数据(参数是原始数据类型的情况)
<script setup>
import { ref, watch } from 'vue'
let count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`count的值变化了,新值:${newValue},旧值:${oldValue}`)
})
const changeCount = () => {
count.value += 10;
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
上面这段代码中:
- 我们使用 ref 定义了一个响应式数据 count,初始值是原始数据类型(数字类型)0。
- 然后使用 watch 函数侦听 count 值的变化。
- 当我们点击按钮“更新count”时,可以看到控制台会打印输出
也就是说:当侦听的数据是用 ref 定义的原数类型的数据时,数据发生变化的时候,就会执行 watch 函数的回调。
2. 使用 watch 侦听 ref 定义的响应式数据(参数是引用数据类型的情况)
<script setup>
import { ref, watch } from 'vue'
let count = ref({ num: 0 })
watch(count, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.value.num += 10;
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
上面这段代码中:
- 我们使用 ref 定义了一个响应式数据 count,初始值是引用数据类型,是一个对象。
- 然后使用 watch 函数侦听 count 值的变化。
- 当我们点击按钮“更新count”时,可以看到界面的 count 值更新了,但控制台并没有打印输出。
这种情况是因为 watch 并没有对 count 进行深度侦听,但是需要注意的是,此时的 DOM 是能够更新的,
要想深度侦听,只需要加一个对应的参数即可,{ deep: true }。
<script setup>
import { ref, watch } from 'vue'
let count = ref({ num: 0 })
watch(
count,
() => {
console.log(`count的值发生变化了`)
},
{ deep: true }
)
const changeCount = () => {
count.value.num += 10;
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
此时,我们点击按钮,DOM更新了,控制台也打印输出了。
我们对上面的代码再进行改造下,直接侦听 count.value,但是不深度侦听,看看 DOM 有没有更新并且控制台有没有打印输出。
<script setup>
import { ref, watch } from 'vue'
let count = ref({ num: 0 })
watch(count.value, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.value.num += 10;
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
可以看到,DOM更新了,控制台也打印输出了,这是什么原因呢?
我们打印一下 count.value 看看,发现打印出来的结果是一个 Proxy 代理对象。因为对象类型的数据经过 ref 函数加工会变成引用对象,而该对象的 value 是 Proxy 类型的。所以我们如果需要监视 Proxy 对象中的数据则需要监视的是 coutn.value 的结构。
3. 使用 watch 侦听 reactive 定义的响应式数据
<script setup>
import { reactive, watch } from 'vue'
let count = reactive({ num: 0 })
watch(count, () => {
console.log(`count的值发生变化了`)
})
const changeCount = () => {
count.num += 10;
}
</script>
<template>
<div class="main">
<p>count: {{ count }}</p>
<button @click="changeCount">更新count</button>
</div>
</template>
上面这段代码中:
- 我们使用 reactive 定义了一个响应式数据 count,传入的是一个对象。
- 然后使用 watch 函数侦听 count 值的变化。
- 当我们点击按钮“更新count”时,可以看到界面的 count 值更新了,控制台也有打印输出。
从上面代码中我们可以看到,用 watch 函数侦听 reactive 数据时,不需要添加 deep 属性,也能够对其深度侦听。