ref和reactive对比终于学会了
文章较长,五千字数左右,可能需要我们费点时间阅读。
感兴趣的可以直接公众号【林太白】关注,持久更新面试!
ref和reactive对比
简单总结
javascript
【ref】
用于包装"基本类型数据"
也能包装引用类型数据(String、Number、Boolean、Undefined、Null、Symbol)
【reactive】
只能用于包装"引用类型数据" (Object、Array、Map、Set 等),不能包装基本类型
写法一览
javascript
// ref
// 模板中使用 ref,无需 .value(Vue 自动解包)
<template>
<div>{{ count }}</div> // ✅ 正确,无需 count.value
</template>
// 脚本中必须用 .value
<script setup>
const count = ref(0);
console.log(count.value); // ✅ 正确,必须 .value
count.value = 1; // ✅ 正确,必须 .value
</script>
// reactive
// reactive 无论在脚本还是模板,都无需 .value
<template>
<div>{{ user.name }}</div> // ✅ 正确
</template>
<script setup>
const user = reactive({ name: "张三" });
user.name = "李四"; // ✅ 正确
</script>
ref
定义
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性.value
javascript
function ref<T>(value: T): Ref<UnwrapRef<T>>
interface Ref<T> {
value: T
}
官方描述
ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。
如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。
若要避免这种深层次的转换,请使用 shallowRef() 来替代。
通常我们经常会使用ref来处理一些原始值(如数字、字符串、布尔值等)在 Vue 的响应式系统中工作
核心原理
1、包装原始值:将原始值包装在一个具有 .value 属性的对象中。
2、响应式转换:使用 reactive 函数使这个对象成为响应式的。
3、依赖追踪:当访问 .value 时,会进行依赖收集;当修改 .value 时,会触发依赖更新
4、模板中自动解包
模板中使用:在模板中,ref 会自动解包,不需要.value
ref的简化实现原理大致如下
javascript
function ref(rawValue) {
// 创建一个 reactive 对象,包装原始值
const r = {
value: rawValue
}
// 将对象转换为响应式
return reactive(r)
}
包裹以后我们需要添加依赖追踪和触发更新的逻辑
javascript
function ref(rawValue) {
// 创建一个 reactive 对象,包装原始值
const r = {
// 标记这是一个 ref
__v_isRef: true,
value: null
}
// 将值转换为响应式
r.value = reactive({
value: rawValue
})
return r
}
reactive
定义
在 Vue 3 中,reactive 是用来创建响应式对象的。
官方介绍:返回一个对象的响应式代理
javascript
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
如果我们想保留对对象顶层次访问的响应性,可以使用 shallowReactive() 作替代
注意坑
1、reactive包装基本类型时,不会报错,但没有响应式效果。
reactive 底层依赖 Proxy,而 Proxy 只能代理对象/数组,无法代理基本类型,所以会直接返回原数据,失去响应式能力。
2、reactive 直接赋值、解构、属性赋值给普通变量,都会导致响应式丢失,用 ref 或 toRefs/toRef 可解决
核心原理
reactive基于Proxy实现,可以创建一个对象的深层响应式版本
拦截对象的所有操作(get、set、delete、has)
访问或修改属性时,会触发依赖收集和派发更新
只能用于对象类型,不能用于基本类型
赋值清空
在vue3之中我们最常使用的就是赋值清空这一步
直接重新赋值
一种简单的方式是直接将响应式对象重新赋值为一个空对象或初始状态。
缺点是,如果你有多个地方引用 state,重新赋值会改变引用,可能会导致不希望的副作用。
plain
import { reactive } from 'vue';
const state = reactive({
name: 'Alice',
age: 25
});
// 清空对象
state = reactive({}); // 重新赋值为一个新的空对象
逐个属性删除
如果你不想改变对象的引用,可以逐个属性删除对象中的数据:
保留了对象的引用,但它会删除所有属性,因此,响应式对象将变成一个空对象。
plain
import { reactive } from 'vue';
const state = reactive({
name: 'Alice',
age: 25
});
// 清空对象的属性
for (const key in state) {
if (state.hasOwnProperty(key)) {
delete state[key];
}
}
使用 Object.assign 重置
如果你有一个初始的默认值,并想要重置对象到初始状态,可以使用 Object.assign() 来将对象重置为默认状态。
比较适合我们有初始状态的时候使用,可以避免直接删除属性,仍然保留了对象的引用。
plain
import { reactive } from 'vue';
const defaultState = {
name: '',
age: 0
};
const state = reactive({
name: 'Alice',
age: 25
});
// 重置为默认状态
Object.assign(state, defaultState);
ref和reactive对比
总结
- **ref:**本质上是底层会创建一个"包装对象"
{ value: 原始数据 }对这个包装对象使用 Proxy 代理,监听包装对象的 value 属性的变化,所以修改时必须操作 .value。 - reactive:本质上是直接对原始引用类型数据进行 Proxy 代理,监听对象的所有属性变化,所以无需 .value,直接操作属性即可触发响应。
区别
数据类型
- ref:支持所有数据类型,包括基本类型(number、string、boolean等)和对象
- reactive:只支持对象类型(包括数组、Map、Set等)
访问方式
- ref:需要通过
.value访问和修改值 - reactive:直接访问和修改属性,不需要额外语法
实现机制
- ref:使用包装对象,内部通过
.value属性暴露值 - reactive:使用Proxy直接代理整个对象
解包行为
- ref:在模板中会自动解包,不需要
.value - reactive:在模板中也会自动解包,保持直接访问属性
深层响应式
- ref:对于对象类型,会递归地将其转换为reactive
- reactive:默认就是深层响应式
实际使用场景
ref
javascript
import { ref } from 'vue';
// 基本类型响应式
const count = ref(0);
const double = computed(() => count.value * 2);
// 对象类型响应式
const userRef = ref({
name: '张三',
age: 25
});
userRef.value.name = '李四'; // 需要通过.value访问
reactive的使用场景
javascript
import { reactive } from 'vue';
// 对象响应式
const state = reactive({
user: {
name: '张三',
age: 25
},
count: 0
});
state.user.name = '李四'; // 直接访问
state.count++; // 直接修改
相同点
1、共享响应式核心机制
依赖收集与派发更新相同:两者都使用相同的依赖收集和派发更新机制
核心依赖机制代码大致如下
javascript
// 简化的依赖收集和触发更新系统
// 定义一个全局变量activeEffect,它是一个函数
let activeEffect = null;
// 定义一个全局变量targetMap,它是一个WeakMap,它的key是target,value是depsMap
const targetMap =new WeakMap();
// 依赖收集
function track(target, key) {
// 1. 检查是否有活动的effect
if (!activeEffect) return;
// 2. 获取或创建target对应的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果depsMap不存在,则创建一个
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 3. 获取或创建key对应的依赖集合
let deps = depsMap.get(key);
if (!deps) {
// 如果deps不存在,则创建一个Set
deps = new Set();
depsMap.set(key, deps);
}
// 4. 将当前活动的effect添加到依赖集合中
deps.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
// 1.从targetMap中获取target对应的依赖映射
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 2.从依赖映射中获取key对应的依赖集合
const deps = depsMap.get(key);
if (deps) {
// 3. 遍历依赖集合,执行所有 effect
deps.forEach(effect => effect());
}
}
除了依赖更新,同时也使用相同的effect系统来管理副作用
javascript
// effect系统
function effect(fn) {
// 1. 创建一个包装函数 effectFn
const effectFn = ()=> {
// 2. 设置 activeEffect 为当前 effectFn
activeEffect = effectFn;
// 3. 执行原始函数 fn
fn();
// 4. 清除 activeEffect
activeEffect = null;
};
// 5. 执行包装函数,触发依赖收集
effectFn();
// 6. 返回 effectFn,以便后续可以手动调用或清除
return effectFn;
}
2、共享标记系统
使用相同的标记来标识响应式对象
javascript
//(2)共享的响应式标记
// 标记为响应式对象
const reactiveMarker = '__v_isReactive';
const refMarker = '__v_isRef';
function isReactive(value) {
// 判断是否为响应式对象
return value && value[reactiveMarker] === true;
}
// 判断是否为 ref 对象
function isRef(value) {
return value && value[refMarker] === true;
}
3、共享的对象处理逻辑
在部分对于对象的处理上两者都是使用相同的工具函数
javascript
//(3)共享的对象处理逻辑
//判断是否是对象
function isObject(value) {
return value !== null && typeof value === "object";
}
// 判断是否是数组
function isArray(value) {
return Array.isArray(value);
}
// 判断是否只读
function isReadonly(value) {
return value && value._v_isReadonly === true;
}
4、共享的代理/拦截机制
实现方式不同,但两者都实现了拦截访问和修改的机制
javascript
//(4)共享的代理/拦截机制
// ref的拦截方式(通过getter/setter)
function ref(value) {
return {
[refMarker]: true, //标记为 ref 对象
_value:value, // 原始值
get value() {
track(this,"value"); // 依赖收集
return this._value;
},
set value(newvalue){
this._value = newvalue;
trigger(this,"value"); // 触发更新
}
}
}
// reactive的拦截方式(通过Proxy)
function reactive(value){
return new Proxy(value,{
get(target,key){
// 检查是否是内部标记
if(key === reactiveMarker) return true;
track(target,key); // 追踪依赖关系
const res=Reflect.get(target, key); // 获取原始值
return isObject(res)? reactive(res):res; // 如果值是对象,递归处理
},
set(target,key,newvalue){
const oldValue = target[key]; // 获取旧值
const result = Reflect.set(target, key, value); //使用 Reflect.set设置新值
if(oldValue !== newvalue){
trigger(target,key); // 触发更新
}
return result;
}
})
}
5、共享自动解包机制
两者在组合使用时共享自动解包逻辑
javascript
// 在reactive中自动解包ref
function get(target,key,receiver){
// 检查是否是内部标记
if (key === refMarker) return true;
const res = Reflect.get(target, key, receiver); // 获取原始值
// 如果值是ref,返回其value
if(isRef(res)){
return res.value;
}
// 如果值是对象,递归处理
if(isObject(res)){
return reactive(res);
}
return res;
}
// 在ref中自动包装reactive
function set(target,key,value,receiver){
if(isRef(value)){
value = value.value;
}
// ... 设置逻辑
}
6、共享的深度响应式处理
两者都支持深度响应式处理
javascript
// 深度响应式处理
function deepReactive(value) {
if (isObject(value)) {
if (isRef(value)) {
return value;
}
return reactive(value);
}
return value;
}
// 深度ref处理
function deepRef(value) {
if (isObject(value)) {
return ref(reactive(value));
}
return ref(value);
}
7. 共享的计算属性系统
两者都可以与计算属性系统无缝集成:
javascript
// (7)共享的计算属性系统
function computed(getter) {
let value; // 计算属性的值
let dirty = true; // 是否需要重新计算
// 计算属性副作用函数
const effectFn = effect(() => {
// 如果dirty为true,则重新计算
value = getter();
dirty = true;
})
return {
get value() {
if (dirty) {
value = getter();
dirty = false;
}
trackRef(effectFn, 'value');
return value;
}
}
}
// 使用示例
const count = ref(0);
const double = computed(() => count.value * 2);
console.log(double.value); // 0
8. 共享的响应式版本控制
两者都支持响应式版本的控制(如只读、浅层响应式等):
javascript
// (8)共享的响应式版本控制
// 只读ref
function readonlyRef(value) {
return {
[refMarker]:true, // 标记为ref
_value:value, // 原始值
get value() {
trackRef(this, 'value');
return this._value;
},
set value(newValue) {
console.warn('readonly ref cannot be modified');
},
}
}
// 浅层reactive
function shallowReactive(value){
// 只处理对象的第一层
return new Proxy(value,{
// 只处理对象的第一层
get(target, key) {
if(key === reactiveMarker) return true;
track(target, key);
return target[key];
},
// 只处理对象的第一层
set(target, key,value){
const oldValue = target[key]; // 获取旧值
const result = Reflect.set(target, key, value); // 设置新值
if(oldValue !== value){
trigger(target, key);
}
return result;
},
})
}
常见误区(❌格外注意)
赋值响应式丢失
ref 支持"直接赋值",reactive 不支持"直接赋值"(赋值会导致响应式丢失)
ref 直接赋值
javascript
const count = ref(0);
count.value = 1; // ✅ 正常,响应式保留
const user = ref({ name: "张三" });
user.value = { name: "李四" }; // ✅ 正常,响应式保留
reactive 直接赋值
javascript
let user = reactive({ name: "张三" });
user = { name: "李四" }; // ❌ 错误!响应式丢失
原因:reactive 代理"原始对象本身",给 reactive 包装的变量赋值时,相当于把变量指向了一个新的普通对象,原来的 Proxy 代理关系被切断,自然就失去了响应式能力。
就像你有一个遥控器(Proxy)控制电视(原始对象),如果你把遥控器变量指向了另一个新电视,原来的遥控器就控制不了原来的电视了。
ref 代理的是"包装对象的 value 属性",赋值时只是修改了 value 的值(无论是基本类型还是引用类型),Proxy 代理关系依然存在,所以响应式不会丢失。
就类似你有一个带锁的盒子(ref包装对象),你只是更换了盒子里的东西(value),盒子本身(Proxy关系)没变,所以锁(响应式)依然有效。
写法优化
我们常见可以通过一些写法优化处理上面的响应式赋值丢失的问题
javascript
// 响应式丢失的写法 ❌
let user = reactive({ name: "张三" });
// 接口请求后,直接赋值新对象
user = await api.getUserInfo(); // ❌ 响应式丢失,后续修改 user 无效果
// 解决方案1:不直接赋值,修改属性(推荐)
const user = reactive({ name: "", age: 0 });
const res = await api.getUserInfo();
// 逐个修改属性,保留 Proxy 代理关系
user.name = res.name;
user.age = res.age; // ✅ 响应式有效
// 解决方案2:用 ref 包装(适合需要整体替换的场景)
const user = ref({ name: "张三" });
user.value = await api.getUserInfo(); // ✅ 响应式有效,直接替换整个对象
数组/集合赋值误区
我们经常会在数据之中进行数组的重新赋值,但是reactive 和 ref 对于数组的处理略有不同
- reactive 处理数组 :支持直接修改数组的元素、调用数组方法(push、pop、splice 等),都会触发响应式;但不能直接给整个数组赋值(和对象赋值一样,会丢失响应式===替换数组)
- ref 处理数组:需要通过 .value 访问数组,修改元素、调用数组方法时,都要加上 .value,同样支持响应式;且可以直接给 .value 赋值新数组,响应式不会丢失。
reactive 处理数组
javascript
// reactive 处理数组
let list = reactive([1, 2, 3]);
list.push(4); // ✅ 正确,响应式有效
list[0] = 10; // ✅ 正确,响应式有效
list = [4, 5, 6]; // ❌ 错误,响应式丢失
ref 处理数组
javascript
const list = ref([1, 2, 3]);
list.value.push(4); // ✅ 正常工作
list.value[0] = 10; // ✅ 正常工作
list.value = [4, 5, 6]; // ✅ 正常工作
解构reactive赋值失效
【问题】
解构 reactive 包装的对象时,解构出来的属性会变成"普通值",失去响应式能力------因为解构本质是"取值"
【原因】取出的是属性的原始值,不再受 Proxy 监控。
【解决】正常我们写复杂对象并且需要响应式的时候会使用toRefs去改变对象的值
toRefs可以将reactive对象的每个属性,都转换成ref对象,解构后,每个属性依然是响应式的,修改时使用.value
javascript
// 错误示例(响应式丢失)
const product = reactive({
id: 1,
name: "笔记本电脑",
price: 5999,
details: {
brand: "Apple",
model: "MacBook Pro"
}
});
// 错误:直接解构,响应式丢失
const { name, price, details } = product;
name = "新款MacBook"; // ❌ 响应式丢失
price = 6999; // ❌ 响应式丢失
details.brand = "Apple Inc."; // ❌ 响应式丢失
// 解决方案1:不解构,直接访问属性(推荐)
product.name = "新款MacBook";
product.price = 6999;
product.details.brand = "Apple Inc."; // ✅ 响应式有效
// 解决方案2:用 toRefs 解构(保留响应式)
import { toRefs } from "vue";
const product = reactive({
id: 1,
name: "笔记本电脑",
price: 5999,
details: {
brand: "Apple",
model: "MacBook Pro"
}
});
const { name, price, details } = toRefs(product);
name.value = "新款MacBook"; // ✅ 响应式有效
price.value = 6999; // ✅ 响应式有效
// 注意:details 仍然是普通对象,需要进一步处理
reactive对象属性赋值普通变量
reactive 对象的某个属性赋值给普通变量,这个普通变量会失去响应式,本质也是"取出了原始值"。
javascript
<template>
<div>
<h2>计数器</h2>
<p>当前计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { reactive, toRef } from 'vue';
// 错误示例(响应式丢失)
const state = reactive({ count: 0 });
let count = state.count; // 直接解构
count = 1; // ❌ 响应式丢失,页面不会更新
// 解决方案1:直接操作 reactive 对象
function increment() {
state.count += 1; // ✅ 响应式有效
}
// 解决方案2:用 toRef 单独包装
const counter = toRef(state, 'count');
function increment() {
counter.value += 1; // ✅ 响应式有效
}
</script>
数组/集合不当操作导致响应失效
javascript
// 错误示例1:用索引直接替换整个数组元素(针对引用类型元素)
const tasks = reactive([
{ id: 1, title: "学习Vue" },
{ id: 2, title: "写代码" }
]);
// 直接用普通对象替换数组中的元素,会丢失该元素的响应式
tasks[0] = { id: 1, title: "学习React" }; // ❌ 替换后的元素是普通对象,不是响应式的
// 解决方案1:修改元素的属性,不替换整个元素
tasks[0].title = "学习React"; // ✅ 响应式有效
// 解决方案2:用 splice 替换元素(保留响应式)
tasks.splice(0, 1, { id: 1, title: "学习React" }); // ✅ 用 splice 替换,响应式有效
// 错误示例2:直接修改数组的 length
const numbers = reactive([1, 2, 3, 4, 5]);
numbers.length = 0; // ❌ 直接修改 length,会导致响应式丢失,后续 push 无效果
// 解决方案:用 splice 清空数组
numbers.splice(0); // ✅ 响应式有效,清空数组后,后续 push 正常触发响应
shallowRef/shallowReactive浅响应式
【格外注意】
1、只需要监控表层数据时,可以使用shallowRef/shallowReactive浅响应式这种方式,减少Proxy的代理开销,提升页面性能进行性能优化。
2、无法用 shallowRef 包装需要频繁修改深层属性的数据,否则会频繁手动调用 triggerRef,增加负担
shallowReactive 不能直接赋值(和 reactive 一样),赋值会导致响应式丢失
3、浅响应式的核心是"性能优化",不确定是否需要时优先用 ref 和 reactive(深响应式),避免因浅响应式导致的"数据不更新"问题。
shallowRef用法
javascript
import { shallowRef } from "vue";
// 用 shallowRef 包装一个对象
const state = shallowRef({
count: 0,
info: {
name: "计数器",
status: "运行中"
}
});
// 场景1:替换整个 .value(表层变化,✅ 触发响应式)
state.value = {
count: 1,
info: {
name: "计数器",
status: "已停止"
}
}; // ✅ 页面会更新
// 场景2:修改深层属性(❌ 不触发响应式)
state.value.count = 1; // ❌ 页面不更新
state.value.info.status = "已暂停"; // ❌ 页面不更新
// 补充:手动触发响应式
import { triggerRef } from "vue";
state.value.info.status = "已暂停";
triggerRef(state); // ✅ 手动触发,页面会更新
适用场景:
比如"弹窗显示/隐藏"(只需要修改 visible.value = true/false)
"表格数据的整体刷新"(只需要替换整个表格数据),这个时候用shallowRef的性能远远比ref 更好
shallowReactive用法
javascript
import { shallowReactive } from "vue";
// 用 shallowReactive 包装一个配置对象
const config = shallowReactive({
title: "我的应用",
settings: {
theme: "dark",
language: "zh-CN"
}
});
// 场景1:修改表层属性(✅ 触发响应式)
config.title = "新应用"; // ✅ 页面会更新
// ✅ 页面会更新(替换整个settings对象)
config.settings = { theme: "light", language: "en-US" };
// 场景2:修改深层属性(❌ 不触发响应式)
config.settings.theme = "light"; // ❌ 页面不更新
config.settings.language = "en-US"; // ❌ 页面不更新
// 补充:无法手动触发,只能通过修改表层属性触发