前言
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
属性,用于存储和访问实际的值。
html
<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
可以生成基本数据类型的响应式数据,那么它可以用来生成引用类型的响应式数据吗?答案是可以的。
js
const proxy = ref({ name: '张三', age: 10 })


区别来了:
-
当给
ref
传递一个原始类型时,它会简单地将这个值包装在一个对象中,这个对象有一个value
属性指向原始值 -
当给
ref
传递一个对象或数组这样的复杂类型时,Vue 不仅会创建一个value
属性来存储该对象,还会用一个Proxy
对象来包装这个对象,以实现更深层次的响应式。这就意味着当对象更深层次的属性值改变时,这些改变也会被 Vue 自动追踪。
js
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 的响应式系统可以检测到这些变化并触发相应的更新。
js
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 来创建一个响应式代理对象。
js
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 函数实现)来确保这个对象或数组是响应式的。
reactive
reactive
函数接受一个对象作为参数,并返回一个新的响应式对象。这个新对象会"代理"原始对象,使得对原始对象的属性进行读取和修改时,Vue 可以追踪这些变化并相应地更新 DOM。
当你使用 reactive
创建一个响应式对象时,Vue 内部实际上是创建了一个 Proxy 对象。
什么是 Proxy 对象
Proxy 是 ES6 提供的一个新特性,它允许我们创建一个对象的代理,从而可以拦截并定义该对象的基本操作(如属性查找、赋值、枚举、函数调用等)的行为。
在 Vue3 中,reactive
函数使用 Proxy 对象来包装原始对象,并拦截对该对象属性的访问和修改操作。当这些操作发生时,Proxy 可以执行一些额外的逻辑,例如依赖收集和触发更新。
底层实现机制
简化源码:
js
// 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--->依赖收集--->触发更新
原始对象与代理对象的关系
通过一个示例来了解他们之间的关系:
js
// 原始对象
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]
js
// 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
方法。
来看示例:
html
<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 中,如果响应式对象中的某个属性是一个普通对象或数组,并且你在之后将其替换为一个新的对象或数组,那么新的对象或数组将不会是响应式的。
如何理解这段话?看示例:
html
<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
创建的代理对象所包装的原始对象。
html
<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
的修改会影响到原始的响应式对象。


点击按钮后,打印值变化了,但是页面值没有变化,丢失了响应性。
直接解构丢失响应性
html
<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 对象时源对象也会跟着变
html
<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 对象的值,源响应式对象也会跟着变
html
<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
:用于将响应式对象中的所有属性都转换为独立的响应式引用。