Vue 系列之:ref、reactive、toRef、toRefs
前言
setup 函数中默认定义的变量并不是响应式的(即数据变了以后页面不会跟着变),如果想让变量变为响应式的变量,需要使用 ref 和 reactive 函数修饰变量。
响应式对象、proxy 对象、ref 对象、reactive 对象,四者之间的关系
-
proxy 对象、ref 对象、reactive 对象都是具体的对象,而响应式对象是一个概念。
-
Proxy 对象是实现响应式对象的一种方式(Vue2 中通过
Object.defineProperty来实现响应式对象)。 -
ref 对象、reactive 对象都是响应式对象的一种。
-
-
Proxy 对象是 ES6 中引入的一个新特性,用于创建一个对象的代理,从而可以拦截并自定义对象的基本操作。Proxy 对象可以看作是在目标对象之前架设的一层“拦截”,外界对该对象的访问都必须先通过这层拦截。
-
ref 创建的对象本身并不是一个 Proxy 对象,它实际上返回的是一个
RefImpl类型的对象,是一个包含.value属性的响应式对象。 -
在 Vue3 的响应式系统中,当使用 reactive 函数时,它实际上会返回一个 Proxy 对象。也是就说 reactive 对象其实就是 Proxy 对象。但在 Vue3 的 API 层面,我们通常将其称为“响应式对象”或“reactive 对象”,以强调其响应式特性。
ref
ref 函数是 Vue 的响应式 API 之一,用于创建一个响应式的引用对象。它会返回一个响应式的对象,该对象包含一个 value 属性,用于存储和访问实际的值。
<template>
<div class="box">
{{ proxy }}
</div>
</template>
<script setup>
import { ref } from 'vue';
const proxy = ref('测试')
console.log("test:", proxy);
</script>
<style scoped>
.box {
padding: 20px;
}
</style>


我们知道 ref 可以生成基本数据类型的响应式数据,那么它可以用来生成引用类型的响应式数据吗?答案是可以的。
const proxy = ref({ name: '张三', age: 10 })


区别来了:
-
当给
ref传递一个原始类型时,它会简单地将这个值包装在一个对象中,这个对象有一个value属性指向原始值 -
当给
ref传递一个对象或数组这样的复杂类型时,Vue 不仅会创建一个value属性来存储该对象,还会用一个Proxy对象来包装这个对象,以实现更深层次的响应式。这就意味着当对象更深层次的属性值改变时,这些改变也会被 Vue 自动追踪。
const proxy = ref({
name: '张三', age: 10, school: {
name: '提瓦特小学',
addr: '蒙德'
}
})
setTimeout(() => {
proxy.value.school.teacher = '钟离'
console.log("test:", proxy);
}, 2000);

通过示例可以看到,给 ref 对象加了一个不存在的属性,也触发了响应式。那为什么很多文章都说 ref 是浅监听呢?它明明实现了深层次的监听呀?
首先搞懂 ref 是怎么实现响应式的:
ref 的基本实现
对于基本数据类型,ref 是通过创建一个包含 value 属性的对象来实现响应式的。具体来说,ref 会创建一个带有 getter 和 setter 的对象,这样当 value 被读取或修改时,Vue3 的响应式系统可以检测到这些变化并触发相应的更新。
function ref(value) {
const refObject = {
__v_isRef: true,
get value() {
track(refObject, 'get', 'value'); // 跟踪读取操作
return value;
},
set value(newValue) {
if (newValue !== value) {
value = newValue;
trigger(refObject, 'set', 'value'); // 触发更新操作
}
}
};
return refObject;
}
-
__v_isRef:这是一个标志属性,用于标识这个对象是一个ref对象。 -
get value():这是一个 getter 方法,当访问refObject.value时会调用这个方法。在 getter 中,会调用track函数来跟踪读取操作。 -
set value(newValue):这是一个 setter 方法,当修改refObject.value时会调用这个方法。在 setter 中,会检查新值和旧值是否不同,如果不同,则更新值并调用trigger函数来触发更新操作。 -
track函数:用于跟踪依赖关系。当组件读取refObject.value时,track函数会被调用,记录当前的依赖关系。这样,当value发生变化时,Vue 3 可以知道哪些组件需要更新。 -
trigger函数:用于触发更新。当value发生变化时,trigger函数会被调用,通知相关的组件进行更新。
对于引用数据类型,ref 会使用 reactive 来创建一个响应式代理对象。reactive 函数又会使用 Proxy 来创建一个响应式代理对象。
function ref(value) {
const refObject = {
__v_isRef: true,
get value() {
track(refObject, 'get', 'value'); // 跟踪读取操作
return value;
},
set value(newValue) {
if (newValue !== value) {
value = newValue;
trigger(refObject, 'set', 'value'); // 触发更新操作
}
}
};
// 如果 value 是一个对象,则使用 reactive 包装
if (isObject(value)) {
refObject.value = reactive(value);
}
return refObject;
}
function isObject(value) {
return typeof value === 'object' && value !== null;
}
由于 refObject.value 就是一个响应式对象,所以可以深度监听其内部属性的变化。所以 “ref 是浅监听” 这句话是错的!
总结:
-
对于基本数据类型,
ref是通过创建一个包含.value属性的响应式对象来保证响应式的,.value属性的值就是原来的值 -
对于引用数据类型,
.value属性的值是通过reactive函数来创建的一个响应式对象。
为什么 ref 需要使用 .value?
为什么 ref 在设计的时候要使用 .value,而 reactive 就不需要使用 .value?都不使用 .value 不是更好吗?
这是因为 Vue.js 的响应式系统是基于 Proxy 和 Reflect API 的,而这些 API 只能代理对象,不能代理原始类型。因此 ref 函数会先创建一个 ref 对象,该对象有一个 .value 属性:
-
对于基本数据类型,
ref会将这个值直接封装在value属性中; -
对于引用数据类型,
ref也会将这个对象或数组封装在value属性中,但 Vue 会使用Proxy机制(通过 reactive 函数实现)来确保这个对象或数组是响应式的。
ref 一定要使用 .value 吗?
ref 的本质是一个包装对象,其值存储在 .value 属性中,在 JavaScript/TypeScript 代码层面必须通过 .value 访问 / 修改;只有在 Vue 提供的特定 “语法糖场景” 中,Vue 会自动帮你解析 .value,此时可以省略。
模板(Template)中可省略 .value
这是最常见的场景,模板中访问 ref 时,Vue 会自动解包,直接写变量名即可。
<template>
<!-- 无需 .value,直接用 count -->
<div>{{ count }}</div>
<!-- undefined -->
<div>{{ count.value }}</div>
<button @click="count++">加1</button>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
补充:不仅是 {{ }} 插值,所有模板表达式(如 v-if=“count > 0”、@click=“count++”)都会自动解包 ref。
v-model 绑定 ref
在模板中用 v-model 绑定 ref 时,无需 .value
<template>
<!-- 无需 .value -->
<input v-model="inputValue" />
<!-- 输入框无响应,控制台报错 -->
<input v-model="inputValue.value" />
</template>
<script setup>
const inputValue = ref('')
</script>
补充:v-model 本质是语法糖(:modelValue + @update:modelValue),Vue 内部会处理 ref 的赋值,所以必须传 ref 本身,不能传 .value。
如果你写 <input v-model="inputValue.value" />,相当于试图对一个字符串(原始值)做双向绑定,自然无效甚至报错。
reactive 中嵌套的 ref(自动解包)
在 reactive 对象中,ref 会被自动解包,得到原始值
const count = ref(0)
const state = reactive({
count, // 自动解包
other: 'value'
})
// 不需要 .value,访问 state.count 得到的是原始值。
console.log(state.count) // 0
state.count = 10 // 不需要 .value
console.log(state.count.value) // 输出 undefined
state.count.value = 20 // 报错:Cannot create property 'value' on number '10'
注意:仅限于顶层属性,如果 ref 嵌套在深层对象中(比如数组、嵌套对象),不会自动解包!
watch 的第一个参数直接传 ref
watch 监听 ref 时,直接传 ref 对象即可(无需 .value),回调的参数是解包后的值
const count = ref(0)
// 源直接传 count,无需 .value
watch(count, (newVal) => {
console.log(newVal) // newVal 是解包后的值,无需 .value
})
watch(count.value, (newVal) => {
console.log(newVal) // 永远不会触发!原因:watch 监听的是 “响应式数据”,count.value 是原始值(数字 0),不是响应式对象,所以 watch 无法感知其变化。
})
补充:watch(count, ...) 相当于 watch(() => count.value, ...),但更高效(Vue 内部做了优化)。
父组件通过 props 传递 ref 数据
父组件(Parent.vue):
<template>
<Child :count="count" />
<button @click="count++">父组件加1</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
</script>
子组件(Child.vue):
<template>
<div>
{{ count }}
</div>
</template>
<script setup>
const props = defineProps({
count: Number,
})
console.log('count:', props.count) // 0
console.log('count.value', props.count.value) // undefined
watch(
() => props.count,
newVal => {
console.log('watch count:', newVal)
}
)
</script>
补充:props 接收的是解包后的值(即原始值),不再是 ref。所以子组件无法通过 props.count.value 访问,也不能修改(Vue 会警告)。
如果父组件想让子组件能修改这个值,应该用 v-model 或传递 ref 并配合 defineModel()(Vue 3.4+)或自定义事件。
setup() 函数的返回对象中
注意是返回对象。
export default {
setup() {
const message = ref('Hello')
// 内部必须使用 .value
const changeMessage = () => {
message.value = 'Changed'
}
// 返回的 ref 会被自动解包
return { message, changeMessage }
}
}
computed 依赖 ref 时(不会自动解包)
注意:computed 的回调函数中,依赖的 ref 【不会】被自动解包
const count = ref(0)
// 报错
const double1 = computed(() => count * 2)
// 正常运行
const double2 = computed(() => count.value * 2)
总结
| 场景 | 是否需要 .value | 说明 |
|---|---|---|
模板({{}}、指令、事件) | ❌ 不需要 | Vue 自动解包 |
v-model 绑定 | ❌ 不需要 | 传 ref 本身 |
reactive 的直接属性 | ❌ 不需要 | 自动解包 |
reactive 的嵌套属性/数组 | ✅ 需要 | 不会自动解包! |
computed / watchEffect / 函数体内 | ✅ 需要 | 普通 JS 上下文 |
watch(ref) | ❌ 不需要(传 ref) | 回调参数是解包后的值 |
| Props 接收 | ❌ 不需要(且不能用) | 已是原始值 |
setup() 返回对象 | ❌ 模板中不需要 | 内部逻辑仍需 .value |
reactive
reactive 函数接受一个对象作为参数,并返回一个新的响应式对象。这个新对象会“代理”原始对象,使得对原始对象的属性进行读取和修改时,Vue 可以追踪这些变化并相应地更新 DOM。
当你使用 reactive 创建一个响应式对象时,Vue 内部实际上是创建了一个 Proxy 对象。
什么是 Proxy 对象
Proxy 是 ES6 提供的一个新特性,它允许我们创建一个对象的代理,从而可以拦截并定义该对象的基本操作(如属性查找、赋值、枚举、函数调用等)的行为。
在 Vue3 中,reactive 函数使用 Proxy 对象来包装原始对象,并拦截对该对象属性的访问和修改操作。当这些操作发生时,Proxy 可以执行一些额外的逻辑,例如依赖收集和触发更新。
底层实现机制
简化源码:
// 1. reactive 是一个函数
function reactive(target) {
// 1. 它返回一个 Proxy 对象
return new Proxy(target, {
// 2. 在读取属性时(即执行 get 操作)会进行依赖收集
get(target, key, receiver) {
// 3. 依赖收集
track(target, key);
const result = Reflect.get(target, key, receiver);
// 如果结果是对象,递归使其成为响应式
return typeof result === 'object' && result !== null ? reactive(result) : result;
},
// 4. 当属性值发生变化时(即执行 set 操作)会触发更新
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 5. 触发更新
trigger(target, key);
}
return result;
}
});
}
// 假设的 track 和 trigger 函数
// 依赖收集
function track(target, key) {
/*
当访问响应式对象的属性时,
Vue会记录下当前正在执行的副作用函数(effect),
这些函数依赖于该属性的值。这个过程称为依赖收集。
*/
}
// 触发更新
function trigger(target, key) {
/*
当响应式对象的属性发生变化时,
Vue会通知所有依赖该属性的副作用函数重新执行,
从而实现视图的自动更新。这个过程称为触发更新。
*/
}
Vue3 的响应式系统通过依赖收集来追踪哪些响应式数据被哪些副作用函数(effect)所依赖。当访问响应式数据时,Vue 会记录下当前正在执行的副作用函数,并建立数据与副作用之间的依赖关系。
当响应式数据发生变化时,Vue 会触发更新操作。这个更新操作会遍历依赖关系图,并逐个执行依赖的副作用函数,从而实现视图的自动更新。
简单来说就是:接收对象—>创建 Proxy—>依赖收集—>触发更新
原始对象与代理对象的关系
通过一个示例来了解他们之间的关系:
// 原始对象
const obj = {
name: '张三',
age: 18
};
// 处理器对象,定义拦截行为
const handler = {
// 拦截属性读取操作
get(target, property) {
console.log(`Getting ${property}`);
return target[property];
},
// 拦截属性设置操作
set(target, property, value) {
console.log(`Setting ${property} to ${value}`);
target[property] = value;
return true;
}
};
// 创建代理对象
const proxy = new Proxy(obj, handler);
obj.age = 19
// proxy.age = 20
console.log("obj.age:", obj.age)
console.log("proxy.age:", proxy.age)

可以看到,当原始对象的属性值发生变化时,代理对象的属性值也发生了变化,这是因为代理对象读取的其实还是原始对象的属性值:return target[property]
// obj.age = 19
proxy.age = 20
console.log("obj.age:", obj.age)
console.log("proxy.age:", proxy.age)

可以看到,当代理对象的属性值发生变化时,原始对象的属性值也发生了变化,这是因为改变代理对象的属性值,其实就是在改变原始对象的属性值:target[property] = value
问题来了:
当响应式数据发生变化时,Vue 会触发更新操作,这很好理解, 但是当原始对象发生变化时,Vue 也会触发更新操作吗?因为上面的例子中当使用 obj.age = 19 时,并没有触发 set 方法。
来看示例:
<template>
<div class="box">
<div>{{ obj }}</div>
<div>{{ proxy }}</div>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
// 原始对象
let obj = {
name: '张三',
age: 18
};
let proxy = reactive(obj)
setTimeout(() => {
obj.age = 19
console.log("obj:", obj);
console.log("proxy:", proxy);
}, 1000);
watch((proxy), (newVal, oldVal) => {
console.log("newVal:", newVal);
console.log("oldVal:", oldVal);
}, { deep: true })
</script>
<style scoped>
.box {
padding: 20px;
}
</style>


可以看到,当修改原始对象的属性值时,这个修改会反应到 Proxy 对象中(打印的 proxy 值发生了变化),但是 Vue 的响应式系统并没有做出更新(页面中的 age 值没有发生变化,watch 中的内容也没有执行)
所以说:直接操作原始对象会触发 Vue 响应式更新这句话是错的。
在 Vue3 中,如果响应式对象中的某个属性是一个普通对象或数组,并且你在之后将其替换为一个新的对象或数组,那么新的对象或数组将不会是响应式的。
如何理解这段话?看示例:
<template>
<div class="box">
<p>proxy:{{ proxy.user.name }}</p>
<p>obj:{{ obj.name }}</p>
<button @click="test">测试</button>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
const proxy = reactive({
user: { name: '张三' }
});
const obj = { name: '李四' };
function test() {
proxy.user = obj;
setTimeout(() => {
obj.name = '王五';
console.log("proxy:", proxy);
console.log("obj:", obj);
}, 1000);
}
</script>
<style scoped>
.box {
padding: 20px;
}
</style>


当点击按钮后,页面中显示的是"李四"而不是"王五",但是打印的值都是"王五",说明是视图没有更新,即新对象 obj 不是响应式的。
非响应式转换
将非响应式对象转换成响应式对象可以用 ref 或 reactive,那么将响应式对象转换为非响应式对象有哪些方法呢?
toRaw 函数
该函数的主要作用是获取一个由 reactive 或者 ref 创建的代理对象所包装的原始对象。
<template>
<div class="box">
<p>proxy:{{ proxy.name }}</p>
<p>obj:{{ obj.name }}</p>
<button @click="test">测试</button>
</div>
</template>
<script setup>
import { ref, reactive, toRaw } from 'vue';
const proxy = reactive({
name: "张三"
});
let obj = toRaw(proxy)
function test() {
obj.name = '李四';
console.log("proxy:", proxy);
console.log("obj:", obj);
}
</script>
<style scoped>
.box {
padding: 20px;
}
</style>
toRaw 返回的是原始数据的引用。因此,对 rawData 的修改会影响到原始的响应式对象。


点击按钮后,打印值变化了,但是页面值没有变化,丢失了响应性。
直接解构丢失响应性
<template>
<div class="box">
<p>proxy:{{ proxy.name }}</p>
<p>obj:{{ name }}</p>
<button @click="test">测试</button>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
const proxy = reactive({
name: "张三"
});
let { name } = proxy
function test() {
name = '李四';
console.log("proxy:", proxy);
console.log("name:", name);
}
</script>


点击按钮后,页面值没有变化,丢失了响应性。
Vue2 动态添加的新属性
在 Vue2 中,动态添加的新属性默认不是响应式的,这是因为 Vue2 的响应式系统是基于 ES5 的 Object.defineProperty 方法实现的,该方法无法拦截对象属性的添加和删除。
但是 Vue3 使用了 ES6 的 Proxy 代理对象来实现响应式,动态添加的新属性默认是响应式的。
ref 与 reactive 的区别
当你通过 ref 包装一个对象时,你实际上得到的是一个包含 .value 属性的对象,这个 .value 属性指向你原始的对象。
-
对于基本数据类型,
.value属性的值就是原来的值 -
对于引用数据类型,
.value属性的值是通过reactive函数来创建的一个响应式对象。
reactive 不会包装对象在另一个对象中,而是直接使对象本身变为响应式的。
toRef
toRef 使源响应式对象的某个属性成为一个 ref 对象。它接收两个参数:源响应式对象和属性名,返回一个 ref 数据。
注意点:
-
“源响应式对象”,即源对象需要是 ref 对象或 reactice 对象
-
“返回一个 ref 数据”,即要用 .value 才能获取到实际值
-
如果源对象是 ref 对象,则要使用 .value;reactive 对象则不需要
-
修改返回的 ref 对象时源对象也会跟着变
<template>
<div class="box">
<p>refObj:{{ refObj }}</p>
<p>reactiveObj:{{ reactiveObj }}</p>
</div>
</template>
<script setup>
import { ref, reactive, toRef } from 'vue';
const proxy1 = ref({
name: '张三', age: 10, school: {
name: '提瓦特小学',
addr: '蒙德'
}
})
const proxy2 = reactive({
name: '张三', age: 10, school: {
name: '提瓦特小学',
addr: '蒙德'
}
})
let refObj = toRef(proxy1.value, 'school') // ref 源对象需要使用 .value
let reactiveObj = toRef(proxy2, 'school')
console.log("refObj:", refObj);
console.log("reactiveObj:", reactiveObj);
</script>
<style scoped>
.box {
padding: 20px;
}
</style>

在 Vue3 中,toRef 函数的作用是使响应式对象的属性也成为响应式对象。响应式对象的属性本来就是响应式的,为什么还需要用 toRef 函数去转换呢?也就是说 toRef 的实际使用场景有哪些?
-
解构响应式对象时保持响应性:当你解构一个响应式对象时,得到的属性会失去它们的响应性,因为它们不再是响应式对象的一部分。使用
toRef可以让你在解构后仍然保持这些属性的响应性。 -
传递响应式属性作为 props:当你将响应式对象的属性作为 prop 传递给子组件时,如果直接传递属性本身,那么子组件将接收到属性的当前值,而不是一个响应式引用。这意味着如果父组件中的属性值发生变化,子组件将不会接收到这个更新,除非使用了
.sync修饰符或v-model。但是,使用toRef可以创建一个响应式引用,并将其作为 prop 传递,这样子组件就可以响应父组件中属性值的变化了。
toRefs
toRefs 它接受一个响应式对象作为参数,并返回一个新的普通对象,普通对象的每个属性都是一个 ref 对象。
注意点:
-
返回的是一个普通对象,不是响应式对象
-
返回的普通对象的每个属性都是 ref 对象
-
改变属性的 ref 对象的值,源响应式对象也会跟着变
<template>
<div class="box">
<p>proxy:{{ proxy }}</p>
<p>toRefsObj:{{ toRefsObj }}</p>
</div>
</template>
<script setup>
import { reactive, toRefs } from 'vue';
const proxy = reactive({
name: '张三', age: 10, school: {
name: '提瓦特小学',
addr: '蒙德'
}
})
let toRefsObj = toRefs(proxy)
console.log("toRefsObj:", toRefsObj);
setTimeout(() => {
toRefsObj.name.value = '李四'
}, 1000);
</script>
<style scoped>
.box {
padding: 20px;
}
</style>
1 秒钟后页面值刷新了,表明是响应式的:

toRefsObj 是普通对象:

toRef 和 toRefs 最大的区别就是:
-
toRef:用于将响应式对象中的单个属性转换为一个独立的响应式引用(ref)。 -
toRefs:用于将响应式对象中的所有属性都转换为独立的响应式引用。
shallowRef
ref 是深层监听,即无论嵌套多少层对象,只要有一个属性值发生变化,都会触发响应。
shallowRef 则仅对.value的赋值操作作出响应,而内部对象的属性变化不会触发更新。
例如:
const test = shallowRef({ name: '张三' })
// ✅ 情况1:整体替换 .value
test.value = { name: '王五' }
// ❌ 情况2:直接修改内部属性
test.value.name = '李四'
// 结果:数据变了,但视图【不会更新】
// ✅ 情况3:使用 triggerRef 来强制更新
test.value.name = '王五'
triggerRef(test)
shallowReactive
与 shallowRef 类似,都是浅层响应式,即只监听第一层,不监听深层属性。
<template>
<div class="el-container">
{{ test }}
</div>
</template>
<script setup>
let test = shallowReactive({ name: '张三', aaddress: { city: '北京' } })
// 触发
setTimeout(() => {
test.name = '李四'
}, 1000)
// 不触发,因为只监听第一层,不监听深层属性
setTimeout(() => {
test.aaddress.city = '上海'
}, 2000)
// 不触发,test 已经变成普通对象了,不是 shallowReactive 对象了
setTimeout(() => {
test = { age: 10 }
}, 3000)
</script>
注意:shallowReactive 没有 triggerRef 这种强制触发机制。
更多推荐
所有评论(0)