当前位置: 首页 > news >正文

【Vue 3 响应式系统深度解析:reactive vs ref 全面对比】

Vue 3 响应式系统深度解析:reactive vs ref 全面对比

目录

  1. 概述
  2. 响应式系统基础
  3. reactive 深度分析
  4. ref 深度分析
  5. 底层实现原理
  6. 依赖收集机制演进
  7. 解构和转换工具
  8. 常见误区和陷阱
  9. 技术选型指南
  10. 最佳实践和建议

概述

Vue 3 引入了基于 Proxy 的全新响应式系统,提供了 reactiveref 两个核心 API。本文档将深入分析这两个 API 的设计原理、使用场景、优劣对比,以及在实际项目中的技术选型建议。

核心改进

相比 Vue 2,Vue 3 的响应式系统解决了以下关键问题:

  • 新增属性响应式:不再需要 Vue.set
  • 删除属性响应式:不再需要 Vue.delete
  • 数组索引和长度修改:原生支持响应式
  • Map、Set 等集合类型:完整支持
  • 更好的 TypeScript 支持

响应式系统基础

什么是 reactive

reactive 是用于创建响应式对象的核心 API,基于 ES6 Proxy 实现深度响应式监听。

import { reactive } from 'vue'const state = reactive({count: 0,user: {name: 'John',profile: {age: 25,city: 'Beijing'}},list: [1, 2, 3]
})// 所有操作都是响应式的
state.count++                    // ✅ 响应式
state.user.name = 'Jane'         // ✅ 深度响应式
state.user.profile.age = 26      // ✅ 深度响应式
state.newProp = 'new'           // ✅ 新增属性响应式
delete state.count              // ✅ 删除属性响应式
state.list.push(4)              // ✅ 数组操作响应式
state.list[0] = 100             // ✅ 数组索引响应式

什么是 ref

ref 是用于创建响应式引用的 API,可以包装任何类型的值,通过 .value 属性访问。

import { ref } from 'vue'// 基本类型
const count = ref(0)
const message = ref('Hello')
const isVisible = ref(false)// 对象类型
const user = ref({name: 'John',age: 25
})// 访问和修改
console.log(count.value)        // 0
count.value = 10               // 响应式更新console.log(user.value.name)   // 'John'
user.value.name = 'Jane'       // 响应式更新

reactive 深度分析

基本特性

优势
  1. 使用直观:像操作普通对象一样使用
  2. 深度响应式:自动处理嵌套对象
  3. 完整的对象操作支持:增删改查都是响应式的
const form = reactive({username: '',email: '',profile: {firstName: '',lastName: '',address: {street: '',city: ''}}
})// 直接操作,无需 .value
form.username = 'john'
form.profile.firstName = 'John'
form.profile.address.city = 'Beijing'// 表单验证
const isValid = computed(() => {return form.username && form.email && form.profile.firstName
})
限制和缺点
  1. 类型限制:只能用于对象类型(Object、Array、Map、Set 等)
// ❌ 错误:不能用于基本类型
const count = reactive(0)        // 无效
const message = reactive('hello') // 无效// ✅ 正确:只能用于对象类型
const state = reactive({ count: 0 })
const list = reactive([1, 2, 3])
  1. 解构丢失响应性:这是最大的痛点
const state = reactive({count: 0,name: 'John'
})// ❌ 解构后丢失响应性
const { count, name } = state
console.log(count) // 0 (普通值,不是响应式)
count++           // 不会触发更新// ✅ 需要使用 toRefs 转换
const { count, name } = toRefs(state)
count.value++     // 有效,但需要 .value
  1. 重新赋值问题:不能整体替换对象
let state = reactive({ count: 0 })// ❌ 这样会断开响应式连接
state = reactive({ count: 1 })// ✅ 正确的方式:使用 Object.assign
Object.assign(state, { count: 1 })// 或者修改属性
state.count = 1
  1. 传参限制:需要传递整个对象
// ❌ 传递属性会丢失响应性
function updateCount(count) {count++  // 无效
}
updateCount(state.count)// ✅ 传递整个对象
function updateState(state) {state.count++  // 有效
}
updateState(state)

适用场景

1. 表单数据管理
const loginForm = reactive({username: '',password: '',rememberMe: false,validation: {usernameError: '',passwordError: ''}
})// 直接操作表单数据
const handleSubmit = () => {if (!loginForm.username) {loginForm.validation.usernameError = '用户名不能为空'return}// 提交逻辑...
}
2. 复杂状态管理
const appState = reactive({user: {id: null,profile: {name: '',avatar: '',permissions: []}},ui: {loading: false,theme: 'light',sidebarOpen: true,notifications: []},data: {posts: [],comments: {},pagination: {current: 1,total: 0,pageSize: 10}}
})
3. 需要频繁嵌套操作的场景
const gameState = reactive({player: {position: { x: 0, y: 0 },inventory: {weapons: [],items: [],money: 1000},stats: {health: 100,mana: 50,experience: 0}},world: {currentLevel: 1,enemies: [],treasures: []}
})// 频繁的嵌套操作很方便
gameState.player.position.x += 10
gameState.player.inventory.money -= 50
gameState.player.stats.health -= 10

ref 深度分析

基本特性

优势
  1. 类型灵活:支持任何类型的数据
  2. 可以重新赋值:整体替换值
  3. 明确的访问语义.value 表明这是响应式引用
  4. 更好的 TypeScript 支持
// 基本类型
const count = ref(0)
const message = ref('Hello')
const isLoading = ref(false)// 对象类型
const user = ref({name: 'John',age: 25
})// 可以重新赋值
count.value = 100
user.value = { name: 'Jane', age: 30 }  // 整体替换// TypeScript 类型推导良好
const typedRef: Ref<number> = ref(0)
  1. 组合性好:在 Composition API 中表现优秀
function useCounter(initialValue = 0) {const count = ref(initialValue)const increment = () => count.value++const decrement = () => count.value--const reset = () => count.value = initialValuereturn {count,      // 返回 ref 对象,保持响应性increment,decrement,reset}
}// 使用时保持响应性
const { count, increment } = useCounter(10)
increment() // 有效
限制和注意事项
  1. 需要 .value:在 JavaScript 中访问需要 .value
const count = ref(0)// ❌ 忘记 .value
console.log(count)     // RefImpl 对象,不是值
if (count > 5) { }     // 错误的比较// ✅ 正确使用 .value
console.log(count.value)     // 0
if (count.value > 5) { }     // 正确的比较
  1. 解构仍然有问题:不能解构 .value
const obj = ref({count: 0,name: 'John'
})// ❌ 解构 .value 会丢失响应性
const { count, name } = obj.value
count++  // 不会触发更新// ✅ 正确方式:使用 toRefs
const { count, name } = toRefs(obj.value)
count.value++  // 有效

混合实现机制

ref 的实现是 Object.definePropertyProxy 的混合:

// ref 的简化实现
class RefImpl {constructor(value) {this._rawValue = value// 如果 value 是对象,使用 reactive 包装(Proxy)this._value = isObject(value) ? reactive(value) : value}
}// 使用 Object.defineProperty 定义 .value 属性
Object.defineProperty(RefImpl.prototype, 'value', {get() {track(this, 'get', 'value')  // 收集依赖return this._value},set(newValue) {if (hasChanged(newValue, this._rawValue)) {this._rawValue = newValuethis._value = isObject(newValue) ? reactive(newValue) : newValuetrigger(this, 'set', 'value', newValue)  // 触发更新}}
})

这种设计的访问路径:

const obj = ref({ name: 'John' })
obj.value.name = 'Jane'
//  ↑     ↑
// defineProperty  Proxy
// 拦截 .value    拦截 .name

适用场景

1. 基本类型值
const count = ref(0)
const message = ref('')
const isVisible = ref(false)
const selectedId = ref(null)
2. 需要重新赋值的数据
const currentUser = ref(null)// 可以整体替换
currentUser.value = await fetchUser()
currentUser.value = null  // 登出时清空
3. 组合式函数
function useFetch(url) {const data = ref(null)const error = ref(null)const loading = ref(false)const execute = async () => {loading.value = trueerror.value = nulltry {const response = await fetch(url)data.value = await response.json()} catch (err) {error.value = err.message} finally {loading.value = false}}return { data, error, loading, execute }
}
4. 需要明确响应式语义的场景
// ref 的 .value 明确表明这是响应式引用
const userPreferences = ref({theme: 'dark',language: 'zh'
})// 在函数中明确知道这是响应式的
function updateTheme(preferences) {preferences.value.theme = 'light'  // 明确的响应式操作
}

底层实现原理

Vue 2 vs Vue 3 实现对比

Vue 2:基于 Object.defineProperty
// Vue 2 的响应式实现(简化版)
function defineReactive(obj, key, val) {const dep = new Dep()  // 每个属性一个依赖收集器// 递归处理嵌套对象if (typeof val === 'object' && val !== null) {observe(val)}Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// 收集依赖if (Dep.target) {dep.depend()}return val},set(newVal) {if (newVal === val) returnval = newVal// 如果新值是对象,也要观察if (typeof newVal === 'object' && newVal !== null) {observe(newVal)}// 通知更新dep.notify()}})
}// Vue 2 的限制
const data = { user: { name: 'John' } }
observe(data)// ❌ 这些操作不是响应式的
data.newProp = 'new'      // 新增属性
delete data.user          // 删除属性
data.list = [1, 2, 3]
data.list[0] = 100        // 数组索引
data.list.length = 0      // 数组长度
Vue 3:基于 Proxy
// Vue 3 的 reactive 实现(简化版)
function reactive(target) {if (!isObject(target)) {return target}return createReactiveObject(target, mutableHandlers)
}function createReactiveObject(target, handlers) {return new Proxy(target, handlers)
}const mutableHandlers = {get(target, key, receiver) {// 收集依赖track(target, 'get', key)const result = Reflect.get(target, key, receiver)// 深度响应式:如果属性也是对象,递归包装if (isObject(result)) {return reactive(result)}return result},set(target, key, value, receiver) {const oldValue = target[key]const result = Reflect.set(target, key, value, receiver)// 触发更新if (hasChanged(value, oldValue)) {trigger(target, 'set', key, value, oldValue)}return result},deleteProperty(target, key) {const hadKey = hasOwn(target, key)const result = Reflect.deleteProperty(target, key)if (result && hadKey) {trigger(target, 'delete', key)}return result},has(target, key) {const result = Reflect.has(target, key)track(target, 'has', key)return result},ownKeys(target) {track(target, 'iterate', ITERATE_KEY)return Reflect.ownKeys(target)}
}// Vue 3 的优势:所有操作都是响应式的
const state = reactive({ user: { name: 'John' } })// ✅ 这些操作都是响应式的
state.newProp = 'new'         // 新增属性
delete state.user             // 删除属性
state.list = [1, 2, 3]
state.list[0] = 100          // 数组索引
state.list.length = 0        // 数组长度
state.list.push(4)           // 数组方法

技术对比表

特性Vue 2 (Object.defineProperty)Vue 3 (Proxy)
新增属性❌ 需要 Vue.set✅ 自动响应式
删除属性❌ 需要 Vue.delete✅ 自动响应式
数组索引❌ 需要特殊处理✅ 自动响应式
数组长度❌ 不支持✅ 自动响应式
Map/Set❌ 不支持✅ 完整支持
性能启动时递归遍历所有属性懒响应式,按需代理
兼容性支持 IE8+不支持 IE

依赖收集机制演进

Vue 2 的依赖收集

核心概念
  • Dep(依赖收集器):每个响应式属性都有一个 Dep 实例
  • Watcher(观察者):计算属性、渲染函数、用户 watch 的实例
  • Dep.target:全局变量,指向当前正在计算的 Watcher
// Vue 2 依赖系统的简化实现
class Dep {constructor() {this.subs = []  // 存储依赖这个属性的所有 Watcher}static target = null  // 全局:当前正在计算的 Watcherdepend() {if (Dep.target) {Dep.target.addDep(this)  // Watcher 记录依赖的 Depthis.subs.push(Dep.target)  // Dep 记录依赖的 Watcher}}notify() {this.subs.forEach(watcher => watcher.update())}
}class Watcher {constructor(vm, expOrFn, cb) {this.vm = vmthis.getter = expOrFnthis.cb = cbthis.deps = []  // 这个 Watcher 依赖的所有 Depthis.value = this.get()}get() {Dep.target = this    // 设置当前 Watcherconst value = this.getter.call(this.vm)  // 执行,触发依赖收集Dep.target = null    // 清空return value}addDep(dep) {this.deps.push(dep)}update() {// 响应式更新const newValue = this.get()if (newValue !== this.value) {const oldValue = this.valuethis.value = newValuethis.cb.call(this.vm, newValue, oldValue)}}
}// 使用示例
const vm = new Vue({data: {firstName: 'John',lastName: 'Doe'},computed: {fullName() {// 当这个计算属性执行时:// 1. Dep.target = fullNameWatcher// 2. 访问 this.firstName,firstName 的 dep 收集 fullNameWatcher// 3. 访问 this.lastName,lastName 的 dep 收集 fullNameWatcher// 4. Dep.target = nullreturn this.firstName + ' ' + this.lastName}}
})
存储结构
// Vue 2 的依赖关系是双向存储的:
// 1. 每个 Dep 知道哪些 Watcher 依赖它
const firstNameDep = new Dep()
firstNameDep.subs = [fullNameWatcher, renderWatcher]// 2. 每个 Watcher 知道它依赖哪些 Dep
const fullNameWatcher = new Watcher(...)
fullNameWatcher.deps = [firstNameDep, lastNameDep]

Vue 3 的依赖收集

核心概念
  • effect:副作用函数,替代 Vue 2 的 Watcher
  • activeEffect:全局变量,指向当前正在执行的 effect
  • targetMap:全局 WeakMap,存储所有依赖关系
// Vue 3 依赖系统的简化实现
const targetMap = new WeakMap()  // 全局依赖映射
let activeEffect = null          // 当前正在执行的 effectfunction track(target, type, key) {if (!activeEffect) return// 获取 target 的依赖映射let depsMap = targetMap.get(target)if (!depsMap) {targetMap.set(target, (depsMap = new Map()))}// 获取 key 的依赖集合let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = new Set()))}// 建立双向连接dep.add(activeEffect)           // 这个属性被这个 effect 依赖activeEffect.deps.push(dep)     // 这个 effect 依赖这个属性
}function trigger(target, type, key, newValue, oldValue) {const depsMap = targetMap.get(target)if (!depsMap) returnconst dep = depsMap.get(key)if (dep) {dep.forEach(effect => {if (effect !== activeEffect) {  // 避免无限循环effect()}})}
}function effect(fn) {const _effect = function() {activeEffect = _effect    // 设置当前 effectfn()                      // 执行,触发依赖收集activeEffect = null       // 清空}_effect.deps = []          // 这个 effect 依赖的所有 dep_effect()                  // 立即执行return _effect
}// 使用示例
const count = ref(0)
const name = ref('John')// 创建 effect
effect(() => {// 当这个 effect 执行时:// 1. activeEffect = 这个 effect 函数// 2. 访问 count.value,触发 track(countRef, 'get', 'value')// 3. 访问 name.value,触发 track(nameRef, 'get', 'value')// 4. activeEffect = nullconsole.log(`${name.value}: ${count.value}`)
})
存储结构
// Vue 3 的依赖关系存储在全局 targetMap 中:
targetMap: WeakMap {reactiveObj1: Map {'count': Set([effect1, effect2]),'name': Set([effect3])},refObj1: Map {'value': Set([effect4])}
}// 每个 effect 也记录它依赖的 dep
effect1.deps = [dep1, dep2, dep3]

Watcher vs Effect 对比

特性Vue 2 WatcherVue 3 Effect
实现方式类实例函数
依赖存储分散在各个 Dep 中集中在全局 targetMap
创建方式new Watcher(vm, exp, cb)effect(fn)
类型RenderWatcher, ComputedWatcher, UserWatcher统一的 effect
组合性较复杂简单易组合
性能相对较重更轻量

为什么 Vue 3 要改变设计?

  1. 函数式编程思想:effect 更简洁,易于组合
  2. 统一的响应式系统:ref、reactive、computed 都基于 effect
  3. 更好的性能:全局集中管理,更高效的依赖追踪
  4. 更好的开发体验:API 更简单,心智负担更小

解构和转换工具

解构响应性问题的根本原因

解构操作本质上是取值操作,会破坏响应式引用:

const state = reactive({count: 0,name: 'John'
})// 解构等价于:
const count = state.count  // 取值:得到普通值 0
const name = state.name    // 取值:得到普通值 'John'// 现在 count 和 name 只是普通变量,与原对象无关
count++  // 只是修改局部变量,不会影响 state.count

这个问题对 reactiveref 都存在:

// reactive 解构问题
const reactiveState = reactive({ count: 0 })
const { count } = reactiveState  // 失去响应性// ref 解构问题也存在
const refState = ref({ count: 0 })
const { count } = refState.value  // 同样失去响应性

toRef:单属性转换

toRef 用于将 reactive 对象的单个属性转换为 ref,保持与原对象的响应式连接。

基本用法
const state = reactive({count: 0,name: 'John',age: 25
})// 为单个属性创建 ref
const countRef = toRef(state, 'count')console.log(countRef.value)  // 0
countRef.value = 10          // 等价于 state.count = 10
console.log(state.count)     // 10,保持同步
实现原理
// toRef 的简化实现
function toRef(object, key) {const val = object[key]return isRef(val) ? val : new ObjectRefImpl(object, key)
}class ObjectRefImpl {constructor(source, key) {this._object = sourcethis._key = keythis.__v_isRef = true}get value() {return this._object[this._key]  // 直接访问原对象属性}set value(val) {this._object[this._key] = val   // 直接修改原对象属性}
}
使用场景
  1. 组合式函数中暴露特定属性
function useUser() {const user = reactive({name: 'John',age: 25,email: 'john@example.com',privateKey: 'secret'  // 不想暴露的属性})const updateUser = (newData) => {Object.assign(user, newData)}// 只暴露需要的属性return {userName: toRef(user, 'name'),    // 暴露 nameuserAge: toRef(user, 'age'),      // 暴露 ageupdateUser                         // 暴露更新方法// privateKey 不暴露}
}const { userName, userAge } = useUser()
userName.value = 'Jane'  // 有效
  1. 性能优化:按需创建
const largeState = reactive({// 假设有 100 个属性prop1: 'value1',prop2: 'value2',// ... 98 more properties
})// 只为需要的属性创建 ref,而不是所有属性
const onlyProp1 = toRef(largeState, 'prop1')  // 只创建一个 ref
// 比 toRefs(largeState) 创建 100 个 ref 更高效

toRefs:全属性转换

toRefs 将 reactive 对象的所有属性转换为 ref,通常用于解构。

基本用法
const state = reactive({count: 0,name: 'John',age: 25
})// 转换所有属性为 ref
const stateAsRefs = toRefs(state)
// 等价于:
// {
//   count: toRef(state, 'count'),
//   name: toRef(state, 'name'),
//   age: toRef(state, 'age')
// }// 可以安全解构
const { count, name, age } = toRefs(state)
count.value++  // 等价于 state.count++
name.value = 'Jane'  // 等价于 state.name = 'Jane'
实现原理
// toRefs 的简化实现
function toRefs(object) {if (!isProxy(object)) {console.warn('toRefs() expects a reactive object')}const ret = isArray(object) ? new Array(object.length) : {}for (const key in object) {ret[key] = toRef(object, key)}return ret
}
使用场景
  1. 组合式函数的返回值解构
function useCounter(initialValue = 0) {const state = reactive({count: initialValue,doubled: computed(() => state.count * 2),isEven: computed(() => state.count % 2 === 0)})const increment = () => state.count++const decrement = () => state.count--const reset = () => state.count = initialValuereturn {// 使用 toRefs 允许解构...toRefs(state),increment,decrement,reset}
}// 可以解构使用
const { count, doubled, isEven, increment } = useCounter(10)
console.log(count.value)    // 10
console.log(doubled.value)  // 20
increment()
console.log(count.value)    // 11
  1. 模板中的自动解包
export default {setup() {const state = reactive({message: 'Hello',count: 0})// 返回 toRefs 的结果return {...toRefs(state)}}
}
<template><!-- 在模板中自动解包,不需要 .value --><div>{{ message }}</div><div>{{ count }}</div>
</template>

toRef vs toRefs 对比

特性toReftoRefs
作用范围单个属性所有属性
性能按需创建,更高效为所有属性创建 ref
使用场景暴露特定属性解构使用
API 语义“我需要这个属性的 ref”“我需要解构这个对象”

实际案例对比

错误的解构方式
function badExample() {const state = reactive({user: { name: 'John', age: 25 },posts: [],loading: false})// ❌ 直接解构,失去响应性return {user: state.user,      // 普通对象,不响应式posts: state.posts,    // 普通数组,不响应式loading: state.loading // 普通布尔值,不响应式}
}const { user, posts, loading } = badExample()
user.name = 'Jane'  // 不会触发更新
正确的解构方式
function goodExample() {const state = reactive({user: { name: 'John', age: 25 },posts: [],loading: false})// ✅ 使用 toRefs,保持响应性return {...toRefs(state)}
}const { user, posts, loading } = goodExample()
user.value.name = 'Jane'  // 有效,会触发更新
loading.value = true      // 有效,会触发更新
混合使用方式
function hybridExample() {const state = reactive({user: { name: 'John', age: 25 },posts: [],loading: false,internalConfig: { /* 不想暴露 */ }})const addPost = (post) => state.posts.push(post)const setLoading = (value) => state.loading = valuereturn {// 只暴露需要的响应式属性user: toRef(state, 'user'),posts: toRef(state, 'posts'),loading: toRef(state, 'loading'),// 暴露操作方法addPost,setLoading}
}

常见误区和陷阱

误区 1:ref 可以直接解构

// ❌ 错误理解
const obj = ref({ count: 0, name: 'John' })
const { count, name } = obj.value  // 失去响应性!console.log(count)  // 0(普通值)
count++            // 不会触发更新// ✅ 正确方式
const { count, name } = toRefs(obj.value)
count.value++      // 有效

误区 2:reactive 比 ref 更高级

// ❌ 错误观念:reactive 更高级,应该优先使用
const state = reactive({count: 0,message: ''
})// 实际问题:解构困难,重新赋值困难// ✅ 实际上 ref 在很多场景下更合适
const count = ref(0)
const message = ref('')

误区 3:混淆引用传递和解构

const count = ref(0)// ✅ 这是引用传递,不是解构
function useCount() {return count  // 传递整个 ref 对象的引用
}const myCount = useCount()
myCount.value++  // 有效,操作的是同一个 ref 对象// ❌ 这才是解构,会失去响应性
const { value } = count
value++  // 无效

误区 4:以为 toRefs 创建新的响应式对象

const state = reactive({ count: 0 })
const refs = toRefs(state)// ❌ 错误理解:refs 是独立的响应式对象
refs.count.value = 10
console.log(state.count)  // 实际上会输出 10// ✅ 正确理解:toRefs 创建的 ref 仍然连接到原对象
state.count = 20
console.log(refs.count.value)  // 输出 20,保持同步

误区 5:不理解 reactive 的重新赋值问题

// ❌ 错误方式:以为可以像 ref 一样重新赋值
let state = reactive({ count: 0 })
state = reactive({ count: 1 })  // 断开了响应式连接!// ✅ 正确方式:修改属性或使用 Object.assign
let state = reactive({ count: 0 })
state.count = 1              // 方式1:修改属性
Object.assign(state, { count: 1 })  // 方式2:合并对象

陷阱 1:模板中的解包陷阱

// 在 setup 中
const obj = ref({nested: { count: 0 }
})return {obj
}
<!-- ❌ 错误:以为模板会深度解包 -->
<template><div>{{ obj.nested.count }}</div>  <!-- 需要 obj.value.nested.count -->
</template><!-- ✅ 正确:只有顶层 ref 会自动解包 -->
<template><div>{{ obj.value.nested.count }}</div>
</template>

陷阱 2:computed 和 watch 中的引用陷阱

const state = reactive({ count: 0 })// ❌ 错误:直接传递属性值
const doubled = computed(() => state.count * 2)  // 这样是对的
watch(state.count, (newVal) => {  // ❌ 这样是错的!console.log('count changed')
})// ✅ 正确:传递 getter 函数或 ref
watch(() => state.count, (newVal) => {  // 传递 getterconsole.log('count changed')
})// 或者使用 toRef
const countRef = toRef(state, 'count')
watch(countRef, (newVal) => {  // 传递 refconsole.log('count changed')
})

陷阱 3:组合式函数的返回值陷阱

// ❌ 错误:返回普通值
function badUseCounter() {const count = ref(0)return {count: count.value,  // 返回普通数字,失去响应性increment: () => count.value++}
}// ✅ 正确:返回 ref 对象
function goodUseCounter() {const count = ref(0)return {count,  // 返回 ref 对象,保持响应性increment: () => count.value++}
}

技术选型指南

官方观点的演进

早期观点(Vue 3.0 时期)

Vue 3 刚发布时,官方文档和示例更多推荐使用 reactive

  • 认为 reactive 更接近 Vue 2 的 data 选项
  • 强调 reactive 的直观性和简洁性
  • 推荐表单和复杂状态管理使用 reactive
当前观点(尤雨溪的最新建议)

随着社区实践的深入,尤雨溪和 Vue 团队的观点发生了转变:

“默认使用 ref,非必要不用 reactive”

这种转变的原因:

  1. 解构问题频繁出现:开发者经常踩坑
  2. TypeScript 支持更好:ref 的类型推导更简单
  3. 组合性更强:ref 在 Composition API 中表现更好
  4. 心智负担更小:API 更统一,不容易出错

决策流程图

开始选择响应式 API↓是基本类型?↓是 → 使用 ref↓否↓需要解构使用?↓是 → 使用 ref↓否↓需要重新赋值?↓是 → 使用 ref↓否↓深度嵌套且不解构?↓是 → 可以考虑 reactive↓否↓默认选择 ref

具体场景推荐

优先使用 ref 的场景
  1. 基本类型值
// ✅ 推荐
const count = ref(0)
const message = ref('')
const isLoading = ref(false)
const selectedId = ref(null)
  1. 需要重新赋值的数据
// ✅ 推荐
const currentUser = ref(null)
const formData = ref({})// 可以整体替换
currentUser.value = await fetchUser()
formData.value = await fetchFormData()
  1. 组合式函数
// ✅ 推荐
function useApi(url) {const data = ref(null)const error = ref(null)const loading = ref(false)return { data, error, loading }
}// 使用时不需要额外处理
const { data, error, loading } = useApi('/api/users')
  1. 可能需要解构的场景
// ✅ 推荐
function useForm() {const username = ref('')const password = ref('')const errors = ref({})return { username, password, errors }
}// 解构使用很自然
const { username, password } = useForm()
可以使用 reactive 的场景
  1. 确定不需要解构的复杂嵌套对象
// ✅ 可以使用 reactive
const gameState = reactive({player: {position: { x: 0, y: 0 },inventory: {items: [],weapons: [],money: 1000},stats: {health: 100,mana: 50,experience: 0}},world: {currentLevel: 1,enemies: [],npcs: []}
})// 频繁的嵌套操作
gameState.player.position.x += 10
gameState.player.inventory.money -= 50
gameState.player.stats.health -= 10
  1. 与现有对象结构匹配
// 当你有现成的对象结构
const apiResponse = {data: [...],pagination: { page: 1, total: 100 },filters: { status: 'active' }
}// ✅ 快速转换为响应式
const state = reactive(apiResponse)
  1. 表单对象(不需要解构时)
// ✅ 可以使用 reactive
const loginForm = reactive({username: '',password: '',rememberMe: false,validation: {usernameError: '',passwordError: ''}
})// 作为整体操作,不解构
const handleSubmit = () => {if (!loginForm.username) {loginForm.validation.usernameError = '用户名不能为空'}
}

混合使用策略

在实际项目中,可以根据具体需求混合使用:

function useUserManagement() {// 简单状态用 refconst loading = ref(false)const error = ref(null)const selectedUserId = ref(null)// 复杂嵌套对象用 reactive(不解构)const userList = reactive({data: [],pagination: {current: 1,pageSize: 10,total: 0},filters: {status: 'active',role: 'user',searchText: ''}})// 需要解构的数据用 refconst currentUser = ref({id: null,name: '',email: '',avatar: ''})return {// ref 数据可以直接解构loading,error,selectedUserId,currentUser,// reactive 数据作为整体返回userList}
}

迁移指南:从 Vue 2 到 Vue 3

Vue 2 data 选项迁移
// Vue 2
export default {data() {return {count: 0,user: {name: 'John',age: 25},list: []}}
}// Vue 3 选项 1:使用 reactive(类似 Vue 2)
export default {setup() {const state = reactive({count: 0,user: {name: 'John',age: 25},list: []})return {...toRefs(state)  // 需要 toRefs 才能在模板中使用}}
}// Vue 3 选项 2:使用 ref(推荐)
export default {setup() {const count = ref(0)const user = ref({name: 'John',age: 25})const list = ref([])return {count,user,list}}
}
注意事项
  1. 响应式系统的差异
// Vue 2:需要注意的操作
this.$set(this.user, 'newProp', 'value')  // 新增属性
this.$delete(this.user, 'prop')           // 删除属性
this.$set(this.list, 0, newValue)         // 数组索引// Vue 3:所有操作都是响应式的
user.value.newProp = 'value'              // 新增属性
delete user.value.prop                    // 删除属性
list.value[0] = newValue                  // 数组索引
  1. 计算属性和侦听器
// Vue 2
computed: {fullName() {return this.firstName + ' ' + this.lastName}
},
watch: {count(newVal, oldVal) {console.log('count changed')}
}// Vue 3 with ref
const firstName = ref('John')
const lastName = ref('Doe')
const count = ref(0)const fullName = computed(() => {return firstName.value + ' ' + lastName.value
})watch(count, (newVal, oldVal) => {console.log('count changed')
})

最佳实践和建议

代码组织最佳实践

1. 按功能分组,而不是按类型
// ❌ 按类型分组(不推荐)
function useUserManagement() {// 所有 refconst userId = ref(null)const userName = ref('')const userEmail = ref('')const loading = ref(false)const error = ref(null)// 所有 computedconst isLoggedIn = computed(() => !!userId.value)const userDisplayName = computed(() => userName.value || userEmail.value)// 所有 methodsconst login = () => { /* ... */ }const logout = () => { /* ... */ }return { /* ... */ }
}// ✅ 按功能分组(推荐)
function useUserManagement() {// 用户基本信息const userId = ref(null)const userName = ref('')const userEmail = ref('')const isLoggedIn = computed(() => !!userId.value)// 异步状态const loading = ref(false)const error = ref(null)// 用户操作const login = async (credentials) => {loading.value = truetry {// 登录逻辑} catch (err) {error.value = err.message} finally {loading.value = false}}const logout = () => {userId.value = nulluserName.value = ''userEmail.value = ''}return {// 状态userId, userName, userEmail, isLoggedIn,loading, error,// 操作login, logout}
}
2. 明确的命名约定
// ✅ 好的命名
const isLoading = ref(false)        // 布尔值用 is/has 前缀
const hasError = ref(false)
const userList = ref([])           // 列表用 List 后缀
const selectedUser = ref(null)     // 选中项用 selected 前缀
const currentPage = ref(1)         // 当前项用 current 前缀// ❌ 模糊的命名
const state = ref(false)
const data = ref([])
const item = ref(null)
3. 合理的粒度控制
// ❌ 粒度过细
const userFirstName = ref('')
const userLastName = ref('')
const userAge = ref(0)
const userEmail = ref('')
const userPhone = ref('')
const userAddress = ref('')// ❌ 粒度过粗
const everything = reactive({user: { /* ... */ },posts: { /* ... */ },settings: { /* ... */ },ui: { /* ... */ }
})// ✅ 合理的粒度
const user = ref({firstName: '',lastName: '',age: 0,email: '',phone: '',address: ''
})const posts = ref([])
const settings = ref({theme: 'light',language: 'zh'
})

性能优化建议

1. 避免不必要的响应式包装
// ❌ 不需要响应式的数据也被包装
const config = reactive({apiUrl: 'https://api.example.com',timeout: 5000,retryCount: 3
})// ✅ 静态配置不需要响应式
const config = {apiUrl: 'https://api.example.com',timeout: 5000,retryCount: 3
}const userSettings = ref({theme: 'light',language: 'zh'
})
2. 使用 shallowRef 和 shallowReactive
// 对于大型数据结构,如果不需要深度响应式
const largeDataSet = shallowRef([// 千条数据...
])// 只有替换整个数组才会触发更新
largeDataSet.value = newDataSet  // 触发更新
largeDataSet.value[0].name = 'new'  // 不触发更新(有时这是期望的)
3. 合理使用 readonly
function useUserStore() {const _users = ref([])const addUser = (user) => _users.value.push(user)const removeUser = (id) => {const index = _users.value.findIndex(u => u.id === id)if (index > -1) _users.value.splice(index, 1)}return {users: readonly(_users),  // 对外只读,防止直接修改addUser,removeUser}
}

类型安全建议

1. 明确的 TypeScript 类型
// ✅ 明确的类型定义
interface User {id: numbername: stringemail: stringavatar?: string
}interface ApiResponse<T> {data: Tmessage: stringsuccess: boolean
}const currentUser = ref<User | null>(null)
const userList = ref<User[]>([])
const apiResponse = ref<ApiResponse<User[]> | null>(null)
2. 避免 any 类型
// ❌ 使用 any
const formData = ref<any>({})// ✅ 使用具体类型
interface FormData {username: stringpassword: stringrememberMe: boolean
}const formData = ref<FormData>({username: '',password: '',rememberMe: false
})

调试和开发体验

1. 使用有意义的 ref 名称用于调试
// ✅ 便于调试的命名
const userListLoading = ref(false)
const userListError = ref(null)
const selectedUserId = ref(null)// 在 Vue DevTools 中能清楚看到各个状态
2. 合理的错误处理
function useApi<T>(url: string) {const data = ref<T | null>(null)const error = ref<string | null>(null)const loading = ref(false)const execute = async () => {loading.value = trueerror.value = nulltry {const response = await fetch(url)if (!response.ok) {throw new Error(`HTTP ${response.status}: ${response.statusText}`)}data.value = await response.json()} catch (err) {error.value = err instanceof Error ? err.message : 'Unknown error'console.error('API Error:', err)} finally {loading.value = false}}return { data, error, loading, execute }
}

团队协作建议

1. 统一的编码规范
// 团队约定:组合式函数的返回格式
function useFeature() {// 1. 状态在前const state = ref(initialState)const loading = ref(false)const error = ref(null)// 2. 计算属性在中间const computedValue = computed(() => /* ... */)// 3. 方法在最后const actionA = () => { /* ... */ }const actionB = () => { /* ... */ }// 4. 返回时按类型分组return {// 状态state, loading, error,// 计算属性computedValue,// 方法actionA, actionB}
}
2. 代码审查清单
  • 是否选择了合适的响应式 API(ref vs reactive)?
  • 是否有不必要的响应式包装?
  • 解构操作是否正确处理了响应性?
  • TypeScript 类型是否准确?
  • 是否有合理的错误处理?
  • 命名是否清晰明确?

总结

Vue 3 的响应式系统提供了强大而灵活的 reactiveref API,它们各有优势和适用场景:

核心要点

  1. 技术选型原则:默认使用 ref,特殊场景考虑 reactive
  2. 解构问题:两者都存在解构丢失响应性的问题,需要用 toRefs 解决
  3. 实现差异reactive 基于 Proxy,ref 基于 Object.defineProperty + Proxy 混合
  4. 依赖收集:Vue 3 使用统一的 effect 系统替代 Vue 2 的 Watcher 机制

最终建议

  • 新项目:优先使用 ref,除非确定不需要解构且深度嵌套的复杂对象
  • 迁移项目:逐步将 reactive 重构为 ref,特别是需要解构的场景
  • 团队协作:建立统一的编码规范和审查清单
  • 性能考虑:避免过度包装,合理使用 shallow 版本的 API

Vue 3 响应式系统的设计体现了现代前端框架的发展趋势:更函数式、更灵活、更易于组合。理解其设计原理和最佳实践,将有助于编写更可维护、更高性能的 Vue 3 应用。

http://www.lryc.cn/news/618939.html

相关文章:

  • 【实时Linux实战系列】基于RFID的实时资产追踪系统
  • 当赞美来敲门:优雅接纳的艺术
  • 21.Linux HTTPS服务
  • GitHub的简单使用方法----(5)
  • 文件IO的学习
  • 论文Review 激光动态物体剔除 Dynablox | RAL2023 ETH MIT出品!
  • web前端第二次作业
  • 5G专网项目外场常见业务测试指南(六)-PingInfoView
  • 衡石HENGSHI SENSE6.0亮点功能-应用创作
  • 衡量机器学习模型的指标
  • HDI 线路板,如何突破普通线路板局限?
  • 基恩士3D视觉用于ABB机器人的KeyenceRobotVisionSetup.sys系统模块程序解析(九、KeyAbsMove)
  • centos 7 如何安装 ZipArchive 扩展
  • 百胜软件×华为云联合赋能,“超级国民品牌”海澜之家新零售加速前行
  • C语言栈的实现
  • NY198NY203美光固态闪存NY215NY216
  • 计算机毕设不知道选什么题目?基于Spark的糖尿病数据分析系统【Hadoop+Spark+python】
  • 鲲鹏arm服务器安装neo4j社区版,实现图书库自然语言检索基础
  • 25C机场航班调度程序(JS 100)
  • Neo4j Cypher
  • RK3568 Linux驱动学习——Linux LED驱动开发
  • Linux NAPI 实现机制深度解析
  • 【Oracle APEX开发小技巧16】交互式网格操作内容根据是否启用进行隐藏/展示
  • 2025年渗透测试面试题总结-16(题目+回答)
  • 力扣(LeetCode) ——移除链表元素(C语言)
  • 飞算AI:企业智能化转型的新引擎
  • 【电子硬件】EMI中无源晶振的优势
  • SpringBoot项目部署
  • string 类运算符重载
  • Win10系统Ruby+Devkit3.4.5-1安装