ref() 与 reactive()
下面,我们来系统的梳理关于 ref() 与 reactive() 的基本知识点:
一、响应式编程核心概念
1.1 什么是响应式编程?
响应式编程是一种声明式编程范式,它使数据变化能够自动传播到依赖它的代码部分。在 Vue 中,响应式系统实现了:
- 数据驱动视图:数据变化自动更新DOM
- 依赖追踪:自动跟踪数据依赖关系
- 高效更新:最小化不必要的DOM操作
1.2 Vue 响应式系统演进
版本 | 响应式实现 | 特点 |
---|---|---|
Vue 2 | Object.defineProperty | 拦截getter/setter,不支持数组索引变化 |
Vue 3 | Proxy API | 全面拦截对象操作,支持数组/集合类型 |
二、reactive() 深度解析
2.1 基本使用
import { reactive } from 'vue'const state = reactive({count: 0,user: {name: 'Alice',profile: {level: 5}}
})// 修改属性
state.count++ // 触发更新
state.user.name = 'Bob' // 深层响应
2.2 实现原理
2.3 核心特性
- 深层响应:嵌套对象自动转为响应式
- 类型保留:Array/Map/Set等特殊类型保持原型方法
- 引用稳定:reactive() 返回同一个代理对象
const obj = {} const proxy1 = reactive(obj) const proxy2 = reactive(obj) console.log(proxy1 === proxy2) // true
2.4 局限性
// 1. 原始值无法使用
const count = reactive(0) // 无效// 2. 属性解构丢失响应性
const { count } = state // 非响应式// 3. 新属性添加需要特殊处理
const state = reactive({})
state.newProp = 'value' // 非响应式// 正确添加响应式属性
import { set } from 'vue'
set(state, 'newProp', 'value')
三、ref() 深度解析
3.1 基本使用
import { ref } from 'vue'// 创建ref
const count = ref(0)
const user = ref({ name: 'Alice' })// 模板中使用
// <div>{{ count }}</div> 自动解包.value// JS中访问
console.log(count.value) // 0
count.value++ // 触发更新
3.2 实现原理
// 简化的ref实现
function ref(value) {const refObject = {get value() {track(refObject, 'value') // 依赖收集return value},set value(newVal) {value = newValtrigger(refObject, 'value') // 触发更新}}return refObject
}
3.3 核心特性
- 包装原始值:使基本类型具有响应性
- 模板自动解包:在模板中无需.value
- 响应式替换:整个对象可替换
const user = ref({ name: 'Alice' })// 替换整个对象 user.value = { name: 'Bob' } // 保持响应性
3.4 ref 的特殊用法
// DOM元素引用
const inputRef = ref(null)onMounted(() => {inputRef.value.focus()
})// 组件引用
<ChildComponent ref="child" />const child = ref(null)
child.value.doSomething()// 函数式ref
<div :ref="(el) => { /* 操作元素 */ }"></div>
四、ref() vs reactive() 对比
4.1 核心差异对比
特性 | ref() | reactive() |
---|---|---|
包装对象 | RefImpl 对象 | Proxy 对象 |
值访问 | 需要 .value | 直接访问 |
原始值支持 | ✅ | ❌ |
深层响应 | ✅ | ✅ |
类型保留 | 完整保留 | 部分特殊处理 |
解构响应性 | 支持解构属性 | 需要toRefs |
整体替换 | ✅ | ❌ (会破坏响应性) |
4.2 使用场景指南
场景 | 推荐 | 说明 |
---|---|---|
基本类型值 | ref | 字符串、数字等 |
复杂对象 | reactive | 表单对象、状态对象 |
组件状态 | reactive | 逻辑相关的状态组 |
模板引用 | ref | DOM元素/组件实例 |
函数参数 | ref | 避免响应式对象解构问题 |
组合函数返回值 | ref | 更灵活的响应式值 |
4.3 性能考量
- 创建开销:reactive() 的 Proxy 创建比 ref() 稍重
- 内存占用:ref() 有额外包装对象开销
- 更新效率:两者更新效率相当
- 最佳实践:
// 大型对象使用reactive更高效 const bigData = reactive(largeDataset)// 独立值使用ref const loading = ref(false)
五、响应式系统高级用法
5.1 响应式转换工具
import { toRef, toRefs, isRef, isReactive, isProxy } from 'vue'const state = reactive({ count: 0 })// 对象属性转为ref
const countRef = toRef(state, 'count')// 整个对象转为ref集合
const refs = toRefs(state) // { count: Ref<number> }// 类型检查
console.log(isRef(countRef)) // true
console.log(isReactive(state)) // true
console.log(isProxy(state)) // true
5.2 浅层响应式
import { shallowReactive, shallowRef } from 'vue'// 浅层reactive:只响应根级属性
const shallowState = shallowReactive({nested: { value: 1 } // 内部非响应
})// 浅层ref:不自动解包内部值
const shallowObj = shallowRef({ count: 0 })
shallowObj.value.count++ // 不会触发更新
5.3 自定义Ref
import { customRef } from 'vue'function useDebouncedRef(value, delay = 200) {let timeoutreturn customRef((track, trigger) => {return {get() {track()return value},set(newValue) {clearTimeout(timeout)timeout = setTimeout(() => {value = newValuetrigger()}, delay)}}})
}// 使用
const text = useDebouncedRef('', 500)
5.4 响应式工具函数
import { markRaw, readonly, watchEffect } from 'vue'// 1. 标记非响应式对象
const nonReactiveObj = markRaw({ shouldNotBeTracked: true })// 2. 创建只读响应式对象
const readOnlyState = readonly(state)// 3. 响应式副作用
watchEffect(() => {console.log(`Count: ${state.count}`) // 自动追踪依赖
})
六、响应式原理深度剖析
6.1 依赖收集与触发
6.2 Proxy 拦截器实现
const reactiveHandlers = {get(target, key, receiver) {track(target, key) // 收集依赖const res = Reflect.get(target, key, receiver)if (isObject(res)) {return reactive(res) // 递归响应式}return res},set(target, key, value, receiver) {const oldValue = target[key]const result = Reflect.set(target, key, value, receiver)if (hasChanged(value, oldValue)) {trigger(target, key) // 触发更新}return result}// 省略deleteProperty/has等其他拦截
}
6.3 响应式系统三大核心
-
Effect(副作用):
let activeEffect class ReactiveEffect {run() {activeEffect = thisthis.fn() // 执行过程中收集依赖} }
-
Dep(依赖集合):
class Dep {constructor() {this.subscribers = new Set()}depend() {if (activeEffect) this.subscribers.add(activeEffect)}notify() {this.subscribers.forEach(effect => effect.run())} }
-
TargetMap(目标映射):
const targetMap = new WeakMap()function track(target, key) {if (!activeEffect) returnlet depsMap = targetMap.get(target)if (!depsMap) targetMap.set(target, (depsMap = new Map()))let dep = depsMap.get(key)if (!dep) depsMap.set(key, (dep = new Dep()))dep.depend() }
七、最佳实践与性能优化
7.1 选择指南
场景 | 推荐 | 替代方案 | 原因 |
---|---|---|---|
独立原始值 | ref | reactive | 更简洁 |
相关状态组 | reactive | 多个ref | 逻辑聚合 |
组件props | ref | - | 标准做法 |
全局状态 | reactive | ref | 结构化数据 |
需要解构 | ref + toRefs | reactive | 保持响应性 |
7.2 性能优化策略
-
避免大型响应式对象:
// 避免 const hugeObj = reactive(/* 大型数据集 */)// 推荐:使用shallowRef或手动管理 const data = ref({}) data.value = largeDataset // 替换而非响应式
-
减少不必要的响应式:
// 静态配置无需响应式 const config = markRaw({itemsPerPage: 10,maxRetries: 3 })
-
合理使用计算属性:
const sortedList = computed(() => [...list.value].sort((a, b) => a.id - b.id) )
-
批量更新优化:
import { nextTick } from 'vue'const updateMultiple = () => {state.a = 1state.b = 2nextTick(() => {// DOM更新后执行}) }
7.3 响应式数据模式
// 组合式函数模式
export function useCounter(initial = 0) {const count = ref(initial)const increment = () => count.value++const decrement = () => count.value--return {count,increment,decrement}
}// 在组件中使用
const { count, increment } = useCounter()
八、常见问题与解决方案
8.1 响应性丢失问题
问题:解构导致响应性丢失
const state = reactive({ count: 0 })
const { count } = state // 失去响应性
解决方案:
// 1. 使用toRefs
const { count } = toRefs(state)// 2. 直接访问原对象
state.count// 3. 使用computed
const count = computed(() => state.count)
8.2 循环引用问题
问题:对象循环引用导致无限递归
const obj = reactive({})
obj.self = obj // 循环引用
解决方案:
// 1. 避免循环引用
// 2. 使用markRaw切断响应链
obj.self = markRaw(obj)
8.3 数组操作问题
问题:直接索引修改数组不触发更新
const list = reactive([1, 2, 3])
list[0] = 9 // 不会触发更新
解决方案:
// 1. 使用变异方法
list.splice(0, 1, 9)// 2. 整个替换
list.value = [9, 2, 3]// 3. 使用Vue.set (Vue 3中为set)
import { set } from 'vue'
set(list, 0, 9)