文章目录
- 一、核心机制对比
- 二、底层实现剖析
-
- [1. reactive简单实现](#1. reactive简单实现)
- [2. ref 实现原理](#2. ref 实现原理)
- 三、实战场景对比
-
- [1. 基本类型处理](#1. 基本类型处理)
- [2. 对象类型处理](#2. 对象类型处理)
- [3. 数组处理](#3. 数组处理)
- 四、高级特性差异
-
- [1. 响应式丢失问题](#1. 响应式丢失问题)
- [2. 类型替换场景](#2. 类型替换场景)
- 五、性能对比分析
- 六、最佳实践
-
- [1. 组合式函数规范](#1. 组合式函数规范)
- [2. 表单处理策略](#2. 表单处理策略)
- 七、特殊场景处理
-
- [1. DOM 引用](#1. DOM 引用)
- [2. 第三方库集成](#2. 第三方库集成)
- 八、总结选择策略
-
- [1. 优先使用 ref 的场景](#1. 优先使用 ref 的场景)
- [2. 优先使用 reactive 的场景](#2. 优先使用 reactive 的场景)
- [3. 混合使用](#3. 混合使用)
-
- [3.1 reactive 嵌套 ref](#3.1 reactive 嵌套 ref)
- [3.2 ref 嵌套 reactive](#3.2 ref 嵌套 reactive)
一、核心机制对比
特性 |
ref |
reactive |
包装对象 |
RefImpl类实现 |
Proxy代理对象 |
响应式原理 |
通过 .value 的 getter/setter 拦截 |
深度代理对象的属性访问 |
数据类型支持 |
任意类型(推荐基本类型) |
仅对象/数组/集合类型 |
深层响应式 |
自动展开(对象会转为 reactive) |
默认深层响应 |
模板自动解包 |
支持(顶层属性自动解包) |
不支持(需保持对象引用) |
TS类型推断 |
Ref 类型包裹 |
原始类型推断 |
二、底层实现剖析
1. reactive简单实现
js
复制代码
const reactive = (target) => {
return new Proxy(target, {
get(target, key) {
// 依赖收集
track(target, key)
return Reflect.get(target, key)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
// 触发更新
trigger(target, key)
}
return result
}
// 其他拦截操作...
})
}
- 依赖收集(Track):
- 通过 track 函数将当前正在执行的副作用(effect)与目标对象的属性关联。
- 使用 WeakMap 存储依赖关系,结构为:
js
复制代码
targetMap = WeakMap({
target: Map({
key: Set(effect1, effect2, ...)
})
});
- 触发更新(Trigger):
- 当属性变化时,通过 trigger 函数找到所有关联的副作用并执行。
2. ref 实现原理
ts
复制代码
class RefImpl {
constructor(value) {
this._value = isObject(value) ? reactive(value) : value;
this.dep = new Set(); // 依赖集合
}
get value() {
track(this.dep); // 依赖收集
return this._value;
}
set value(newVal) {
if (this._value !== newVal) {
this._value = isObject(newVal) ? reactive(newVal) : newVal;
trigger(this.dep); // 触发更新
}
}
}
const ref = (value) => new RefImpl(value);
- 依赖收集与触发更新:
- 每个 ref 实例内部维护一个 dep 集合(类似 reactive 的依赖管理)。
- 当通过 .value 访问时,触发 track;修改时触发 trigger。
- 关键设计:
- 值类型包装: 通过 .value 访问值,解决基本类型无法被 Proxy 直接代理的问题。
- 自动解包: 在模板中使用 ref 时,Vue 会自动解包(无需写 .value)。
三、实战场景对比
1. 基本类型处理
ts
复制代码
// 正确用法
const count = ref(0) // ✅ 自动类型推断为 Ref<number>
// 错误尝试
const count = reactive(0) // ❌ 参数必须是对象类型
2. 对象类型处理
ts
复制代码
// ref 处理对象(自动解包)
const user = ref({
name: 'John',
address: {
city: 'New York'
}
})
// 等效于 reactive 写法
const user = reactive({
name: 'John',
address: reactive({
city: 'New York'
})
})
// 访问方式对比
console.log(user.value.name) // ref 需要 .value
console.log(user.name) // reactive 直接访问
3. 数组处理
ts
复制代码
// ref 数组
const listRef = ref([1, 2, 3])
listRef.value.push(4) // ✅ 正确修改方式
// reactive 数组
const listReactive = reactive([1, 2, 3])
listReactive.push(4) // ✅ 直接操作
四、高级特性差异
1. 响应式丢失问题
ts
复制代码
// reactive 的解构问题
const state = reactive({ count: 0 })
const { count } = state // ❌ 失去响应性
// ref 的 解构
const countRef = ref(0)
const count = countRef // ❌ 仍需通过 .value 访问
// 正确解构方式(reactive)
const state = reactive({ count: 0 })
const countRef = toRef(state, 'count') // ✅ 保持响应
2. 类型替换场景
ts
复制代码
// ref 允许整体替换
const data = ref({ items: [] })
data.value = fetchData() // ✅ 响应式更新
// reactive 需保持引用
const data = reactive({ items: [] })
Object.assign(data, fetchData()) // ✅ 正确方式
data = fetchData() ❌ 破坏响应性
五、性能对比分析
操作 |
ref(基本类型) |
reactive(对象) |
差异原因 |
创建速度 |
★★★★☆ |
★★★☆☆ |
Proxy初始化成本较高 |
属性访问 |
★★★★☆ |
★★★☆☆ |
Proxy拦截带来额外开销 |
深层监听 |
★☆☆☆☆ |
★★★★★ |
Proxy自动深度代理 |
内存占用 |
较低 |
较高 |
Proxy对象占用更多内存 |
六、最佳实践
1. 组合式函数规范
ts
复制代码
// 推荐返回 ref 保持灵活性
function useCounter(initial = 0) {
const count = ref(initial)
return { count }
}
// 复杂状态使用 reactive + toRefs
function useUser() {
const state = reactive({
name: '',
age: ''
})
return { ...toRefs(state) }
}
2. 表单处理策略
ts
复制代码
// 复杂表单使用 reactive
const form = reactive({
username: '',
password: '',
preferences: {
theme: 'light',
notifications: true
}
})
// 单个表单字段使用 ref
const searchQuery = ref('')
七、特殊场景处理
1. DOM 引用
获取元素对象或者组件对象只能使用 ref ,reactive无法处理 DOM 引用
html
复制代码
<script setup>
const inputRef = ref(null)
</script>
<templete>
<div ref="inputRef"></div>
</templete>
2. 第三方库集成
js
复制代码
// 需要保持引用的场景
const chartInstance = ref(null)
// 响应式配置对象
const options = reacitve({
title: { text: '热销' },
series: [ ... ]
})
八、总结选择策略
1. 优先使用 ref 的场景
- 基本类型值(string/number/boolean)
- 需要模板自动解包
- 可能被整体替换的对象
- 需要传递给composable函数的参数
2. 优先使用 reactive 的场景
- 复杂嵌套对象
- 需要深度响应式监听
- 表单/配置对象等结构化数据
- 需要直接操作集合类型(Map/Set)
3. 混合使用
3.1 reactive 嵌套 ref
ts
复制代码
const state = reactive({
count: ref(0), // 基础类型 ref ,自动解包
user: ref<User>({ name: '' }) // 对象类型 ref ,自动解包
})
- 行为特点: Vue 会自动解包嵌套在 reactive 中的 ref,访问时无需 .value
ts
复制代码
state.count++ // 直接操作数字(等价于 state.count.value++)
state.user.name = 'john' // 直接操作对象(等价于 state.user.value.name)
- 适用场景
- 混合响应式类型: 当 reactive 对象需要包含基础类型 + 对象类型的混合数据时,用 ref 统一包裹,享受自动解包特性。
- 外部数据注入: 当某个属性需要从外部接收 ref 类型时(如组合式函数返回的 ref),直接将其嵌入 reactive 对象。
3.2 ref 嵌套 reactive
ts
复制代码
const complexRef = ref({
data: reactive({ /*...*/ }), // 嵌套 reactive 对象
status: 'loading' // 普通属性(非响应式)
})
- 行为特点:
- 层级访问: 需要 .value 访问容器内内容
- 响应式范围:
- complexRef.value.data 是响应式对象(reactive 创建)
- complexRef.value.status 是普通字符串(非响应式)
ts
复制代码
complexRef.value.data.key = "value"; // 直接修改 reactive 对象
complexRef.value.status = "success"; // 修改普通属性(非响应式)
- 适用场景:
- 整体替换响应式对象 :如果需要完全替换整个响应式对象,用 ref 包裹可以保持引用。
- 组合非响应式数据: 当需要混合响应式和非响应式数据时,ref 作为容器更灵活。
ts
复制代码
complexRef.value = { // 替换整个对象(触发响应式更新)
data: reactive({ /*...*/ }),
status: 'success'
};