vue3中的ref和reactive
ref 和 reactive 是 Vue3 中用来实现数据响应式的 API
一般情况下,ref 定义基本数据类型,reactive 定义引用数据类型(喜欢用它来定义对象,不用它定义数组,原因后面讲)
理解的 ref 本质上是 reactive 的再封装
ref:
通过 Object.defineProperty()给 value 的属性添加 getter、setter 来实现响应式
一般用来处理基本数据类型,也能处理复杂数据类型,只不过内部会自动将对象转换为 reactive 的代理对象
在 js 中要加.value,在模版中不需要
reactive:
通过 Proxy 对目标对象中的所有属性动态地进行数据劫持,并通过 Reflect 操作对象内部数据来实现响应式
一般用来处理复杂数据类型,会实现递归深度响应式
reactive()
基本用法
在 Vue3 中可以使用 reactive() 创建一个响应式对象或数组:
import { reactive } from "vue";
const state = reactive({ count: 0 });
这个响应式对象其实就是一个 Proxy, Vue 会在这个 Proxy 的属性被访问时收集副作用,属性被修改时触发副作用。
要在组件模板中使用响应式状态,需要在 setup() 函数中定义并返回。
<script>
import { reactive } from "vue";
export default {
setup() {
const state = reactive({ count: 0 });
return {
state,
};
},
};
</script>
<template>
<div>{{ state.count }}</div>
</template>
当然,也可以使用 <script setup> ,<script setup> 中顶层的导入和变量声明可以在模板中直接使用。
<script setup>
import { reactive } from "vue";
const state = reactive({ count: 0 });
</script>
<template>
<div>{{ state.count }}</div>
</template>
响应式代理 vs 原始对象
reactive() 返回的是一个原始对象的 Proxy,他们是不相等的:
const raw = {};
const proxy = reactive(raw);
console.log(proxy === raw); // false
原始对象在模板中也是可以使用的,但修改原始对象不会触发更新。因此,要使用 Vue 的响应式系统,就必须使用代理。
<script setup>
const state = { count: 0 };
function add() {
state.count++;
}
</script>
<template>
<button @click="add">
{{ state.count }}
<!-- 当点击button时,始终显示为 0 -->
</button>
</template>
为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身:
const raw = {};
const proxy1 = reactive(raw);
const proxy2 = reactive(raw);
console.log(proxy1 === proxy2); // true
console.log(reactive(proxy1) === proxy1); // true
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
const raw = {};
const proxy = reactive({ nested: raw });
const nested = reactive(raw);
console.log(proxy.nested === nested); // true
shallowReactive()
在 Vue 中,状态默认都是深层响应式的。但某些场景下,可能想创建一个 浅层响应式对象 ,让它仅在顶层具有响应性,这时候可以使用 shallowReactive()。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
});
// 状态自身的属性是响应式的
state.foo++;
// 下层嵌套对象不是响应式的,不会按期望工作
state.nested.bar++;
注意:浅层响应式对象应该只用于组件中的根级状态。避免将其嵌套在深层次的响应式对象中,因为其内部的属性具有不一致的响应行为,嵌套之后将很难理解和调试。
reactive() 的局限性
reactive() 虽然强大,但也有以下几条限制:
- 仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的原始类型无效。
- 因为 Vue 的响应式系统是通过属性访问进行追踪的,如果直接“替换”一个响应式对象,这会导致对初始引用的响应性连接丢失:
<script setup>
import { reactive } from "vue";
let state = reactive({ count: 0 });
function change() {
// 非响应式替换
state = reactive({ count: 1 });
}
</script>
<template>
<button @click="change">
{{ state }}
<!-- 当点击button时,始终显示为 { "count": 0 } -->
</button>
</template>
- 将响应式对象的属性赋值或解构至本地变量,或是将该属性传入一个函数时,会失去响应性:
const state = reactive({ count: 0 });
// n 是一个局部变量,和 state.count 失去响应性连接
let n = state.count;
// 不会影响 state
n++;
// count 也和 state.count 失去了响应性连接
let { count } = state;
// 不会影响 state
count++;
// 参数 count 同样和 state.count 失去了响应性连接
function callSomeFunction(count) {
// 不会影响 state
count++;
}
callSomeFunction(state.count);
为了解决以上几个限制,ref 闪耀登场了!
ref()
Vue 提供了一个 ref() 方法来允许创建使用任何值类型的响应式 ref 。
基本用法
ref() 将传入的参数包装为一个带有 value 属性的 ref 对象:
import { ref } from "vue";
const count = ref(0);
console.log(count); // { value: 0 }
count.value++;
console.log(count.value); // 1
和响应式对象的属性类似,ref 的 value 属性也是响应式的。同时,当值为对象类型时,Vue 会自动使用 reactive() 处理这个值。
一个包含对象的 ref 可以响应式地替换整个对象:
<script setup>
import { ref } from "vue";
let state = ref({ count: 0 });
function change() {
// 这是响应式替换
state.value = ref({ count: 1 });
}
</script>
<template>
<button @click="change">
{{ state }}
<!-- 当点击button时,显示为 { "count": 1 } -->
</button>
</template>
ref 从一般对象上解构属性或将属性传递给函数时,不会丢失响应性:
const state = {
count: ref(0),
};
// 解构之后,和 state.count 依然保持响应性连接
const { count } = state;
// 会影响 state
count.value++;
// 该函数接收一个 ref, 和传入的值保持响应性连接
function callSomeFunction(count) {
// 会影响 state
count.value++;
}
callSomeFunction(state.count);
ref() 能创建使用任何值类型的 ref 对象,并能够在不丢失响应性的前提下传递这些对象。这个功能非常重要,经常用于将逻辑提取到 组合式函数 中。
// mouse.js
export function useMouse() {
const x = ref(0);
const y = ref(0);
// ...
return { x, y };
}
<script setup>
import { useMouse } from "./mouse.js";
// 可以解构而不会失去响应性
const { x, y } = useMouse();
</script>
ref 的解包
所谓解包就是获取到 ref 对象上 value 属性的值。常用的两种方法就是 .value 和 unref()。 unref() 是 Vue 提供的方法,如果参数是 ref ,则返回 value 属性的值,否则返回参数本身。
ref 在模板中的解包
当 ref 在模板中作为顶层属性被访问时,它们会被自动解包,不需要使用 .value 。下面是之前的例子,使用 ref() 代替:
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<div>
{{ count }}
<!-- 无需 .value -->
</div>
</template>
还有一种情况,如果文本插值({{ }})计算的最终值是 ref ,也会被自动解包。下面的非顶层属性会被正确渲染出来。
<script setup>
import { ref } from "vue";
const object = { foo: ref(1) };
</script>
<template>
<div>
{{ object.foo }}
<!-- 无需 .value -->
</div>
</template>
其他情况则不会被自动解包,如:object.foo 不是顶层属性,文本插值({{ }})计算的最终值也不是 ref:
const object = { foo: ref(1) };
下面的内容将不会像预期的那样工作:
<div>{{ object.foo + 1 }}</div>
渲染的结果会是 [object Object]1,因为 object.foo 是一个 ref 对象。可以通过将 foo 改成顶层属性来解决这个问题:
const object = { foo: ref(1) };
const { foo } = object;
<div>{{ foo + 1 }}</div>
现在结果就可以正确地渲染出来了。
ref 在响应式对象中的解包
当一个 ref 被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现得和一般的属性一样:
const count = ref(0);
const state = reactive({ count });
console.log(state.count); // 0
state.count = 1;
console.log(state.count); // 1
只有当嵌套在一个深层响应式对象内时,才会发生解包。当 ref 作为 浅层响应式对象 的属性被访问时则不会解包:
const count = ref(0);
const state = shallowReactive({ count });
console.log(state.count); // { value: 0 } 而不是 0
如果将一个新的 ref 赋值给一个已经关联 ref 的属性,那么它会替换掉旧的 ref:
const count = ref(1);
const state = reactive({ count });
const otherCount = ref(2);
state.count = otherCount;
console.log(state.count); // 2
// 此时 count 已经和 state.count 失去连接
console.log(count.value); // 1
ref 在数组和集合类型的解包
跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生集合类型的元素被访问时,不会进行解包。
const books = reactive([ref("Vue 3 Guide")]);
// 这里需要 .value
console.log(books[0].value);
const map = reactive(new Map([["count", ref(0)]]));
// 这里需要 .value
console.log(map.get("count").value);
toRef()
toRef 是基于响应式对象上的一个属性,创建一个对应的 ref 的方法。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, "foo");
// 更改源属性会更新该 ref
state.foo++;
console.log(fooRef.value); // 2
// 更改该 ref 也会更新源属性
fooRef.value++;
console.log(state.foo); // 3
toRef() 在想把一个 prop 的 ref 传递给一个组合式函数时会很有用:
<script setup>
import { toRef } from "vue";
const props = defineProps(/* ... */);
// 将 `props.foo` 转换为 ref,然后传入一个组合式函数
useSomeFeature(toRef(props, "foo"));
</script>
当 toRef 与组件 props 结合使用时,关于禁止对 props 做出更改的限制依然有效。如果将新的值传递给 ref 等效于尝试直接更改 props,这是不允许的。在这种场景下,可以考虑使用带有 get 和 set 的 computed 替代。
注意:即使源属性当前不存在,toRef() 也会返回一个可用的 ref。这让它在处理可选 props 的时候非常有用,相比之下 toRefs 就不会为可选 props 创建对应的 refs 。下面就来了解一下 toRefs 。
toRefs()
toRefs() 是将一个响应式对象上的所有属性都转为 ref ,然后再将这些 ref 组合为一个普通对象的方法。这个普通对象的每个属性和源对象的属性保持同步。
const state = reactive({
foo: 1,
bar: 2,
});
// 相当于
// const stateAsRefs = {
// foo: toRef(state, 'foo'),
// bar: toRef(state, 'bar')
// }
const stateAsRefs = toRefs(state);
state.foo++;
console.log(stateAsRefs.foo.value); // 2
stateAsRefs.foo.value++;
console.log(state.foo); // 3
从组合式函数中返回响应式对象时,toRefs 相当有用。它可以使解构返回的对象时,不失去响应性:
// feature.js
export function useFeature() {
const state = reactive({
foo: 1,
bar: 2,
});
// ...
// 返回时将属性都转为 ref
return toRefs(state);
}
<script setup>
import { useFeature } from "./feature.js";
// 可以解构而不会失去响应性
const { foo, bar } = useFeature();
</script>
toRefs 只会为源对象上已存在的属性创建 ref。如果要为还不存在的属性创建 ref,就要用到上面提到的 toRef。

浙公网安备 33010602011771号