作为 Vue 3 响应式系统的核心,各种 ref
类型构成了状态管理的基石。本文将系统性地介绍所有与 ref
相关的 API,通过对比分析帮助开发者深入理解其差异与适用场景。
核心 Ref 类型总览
API | 响应深度 | 典型应用场景 | 是否触发深层更新 | 性能特点 |
---|---|---|---|---|
ref | 深层响应 | 普通变量/对象 | 是 | 中等 |
shallowRef | 浅层响应 | 大型对象/第三方库实例 | 否 | 较高 |
customRef | 自定义 | 需要精细控制依赖追踪 | 自定义 | 取决于实现 |
toRef | 保持响应链接 | 从 reactive 对象提取属性 | 是 | 高 |
toRefs | 保持响应链接 | 解构 reactive 对象 | 是 | 高 |
isRef | - | 类型检查 | - | - |
unref | - | 获取 ref 值 | - | - |
triggerRef | 强制触发 | 配合 shallowRef 使用 | - | - |
深度解析各 Ref 特性
1. 标准 ref
- 全能选手
适用场景:90% 的常规响应式需求
import { ref } from 'vue'
// 基本用法
const count = ref(0)
const obj = ref({ nested: { value: 1 } })
// 特点:
// - 对基本类型创建响应式引用
// - 对对象类型会调用 reactive 进行深度响应
// - 通过 .value 访问/修改
2. shallowRef
- 性能优化利器
使用场景:大型对象/第三方库实例
import { shallowRef, triggerRef } from 'vue'
const state = shallowRef({
bigData: /* 大型数据集 */
})
// 需要手动触发更新
function updateBigData() {
state.value.bigData[0].value = 'new' // 不会自动触发更新
triggerRef(state) // 手动触发
}
与 ref 的关键差异:
-
不自动深度响应
-
需要整体替换或手动触发更新
-
节省了递归响应式的性能开销
适用场景:
-
大型列表/对象
-
频繁变化但不需要视图更新的数据
-
第三方库实例管理
特性 | ref | shallowRef |
---|---|---|
响应式深度 | 深层次响应(递归转换) | 浅层次响应(仅顶层属性) |
性能消耗 | 较高(需要递归追踪) | 较低(只追踪顶层) |
适用场景 | 普通对象/复杂嵌套结构 | 大型对象/不需要深度响应的数据 |
内部实现 | 使用 reactive 包装值 | 直接存储原始值 |
变更检测 | 检测嵌套属性变化 | 仅检测引用变化 |
3. customRef
- 精细控制大师
customRef
是 Vue 3 中提供的一个高阶函数,用于创建自定义的响应式引用(Ref)。它允许开发者更灵活地控制 Ref 的行为,特别是在处理异步操作或复杂逻辑时,能够自定义依赖收集和触发更新的机制。
基本用法
customRef
接收一个工厂函数作为参数,该工厂函数需要返回一个包含 get
和 set
方法的对象。get
方法用于获取当前值,set
方法用于设置新值,并可以手动触发依赖更新。
import { customRef } from 'vue';
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track(); // 依赖收集
return value;
},
set(newValue) {
clearTimeout(timeout); // 清除之前的定时器
timeout = setTimeout(() => {
value = newValue;
trigger(); // 触发更新
}, delay);
}
};
});
}
// 使用示例
const debouncedText = useDebouncedRef('Hello');
console.log(debouncedText.value); // 输出: Hello
debouncedText.value = 'World'; // 延迟 200ms 后更新
应用场景
- 防抖和节流:在处理用户输入(如搜索框)时,使用
customRef
可以轻松实现防抖或节流功能,避免频繁触发更新。 - 异步数据加载:在从 API 获取数据时,可以通过
customRef
自定义数据加载的逻辑,并在数据加载完成后手动触发更新。 - 复杂逻辑封装:当需要在 Ref 中封装复杂逻辑时,
customRef
提供了更大的灵活性,使得代码更易于维护和复用。
注意事项
track
和trigger
是customRef
提供的两个关键函数,分别用于依赖收集和触发更新。确保在get
方法中调用track
,在set
方法中调用trigger
,以保证响应式系统的正常工作。customRef
的返回值是一个普通的 Ref 对象,可以直接在模板或reactive
对象中使用。
通过 customRef
,开发者可以更精细地控制响应式数据的行为,从而满足更复杂的业务需求。
4. toRef
与 toRefs
- 响应链接守护者
在 Vue 3 的 Composition API 中,toRef
和 toRefs
是两个非常重要的工具函数,它们的主要作用是将响应式对象的属性转换为独立的 ref
对象,从而在解构或传递属性时保持其响应性。这两个函数在管理复杂组件的状态时尤为有用,能够确保数据的响应性不被破坏。
1. toRef
函数
toRef
用于从响应式对象中提取单个属性,并将其转换为一个 ref
对象。这个 ref
对象会保持与原始属性的响应性链接,即使原始对象被修改,ref
对象也会同步更新。
语法:
const ref = toRef(reactiveObject, 'propertyName')
示例:
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0,
message: 'Hello, Vue!'
});
const countRef = toRef(state, 'count');
console.log(countRef.value); // 输出: 0
state.count = 10;
console.log(countRef.value); // 输出: 10
在这个例子中,countRef
是一个 ref
对象,它与 state.count
保持响应性链接。当 state.count
发生变化时,countRef.value
也会同步更新。
2. toRefs
函数
toRefs
用于将整个响应式对象的所有属性转换为 ref
对象,并返回一个包含这些 ref
对象的普通对象。这在解构响应式对象时非常有用,可以确保解构后的属性仍然保持响应性。
语法:
const refs = toRefs(reactiveObject)
示例:
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
message: 'Hello, Vue!'
});
const { count, message } = toRefs(state);
console.log(count.value); // 输出: 0
console.log(message.value); // 输出: 'Hello, Vue!'
state.count = 10;
state.message = 'Updated Message';
console.log(count.value); // 输出: 10
console.log(message.value); // 输出: 'Updated Message'
在这个例子中,count
和 message
都是 ref
对象,它们与 state
中的相应属性保持响应性链接。即使 state
被修改,解构后的 count
和 message
也会同步更新。
3. 应用场景
- 解构响应式对象:在组件中使用
toRefs
可以方便地解构响应式对象,同时保持解构后的属性仍然是响应式的。 - 传递单个属性:当需要将响应式对象的某个属性传递给子组件时,可以使用
toRef
来确保传递的属性保持响应性。 - 保持响应性:在复杂的逻辑中,使用
toRef
和toRefs
可以确保数据的响应性不被破坏,避免因解构或传递导致的响应性丢失问题。
4. 注意事项
toRef
和toRefs
返回的ref
对象是只读的,不能直接修改它们的值。如果需要修改,应该通过原始响应式对象进行修改。- 使用
toRefs
时,返回的对象是一个普通对象,而不是响应式对象。因此,不能直接对这个对象进行响应式操作。
关键特点:
特性 | toRef | toRefs |
---|---|---|
作用对象 | 单个属性 | 整个对象 |
源对象类型 | reactive/props | reactive |
典型用途 | 提取特定属性 | 解构返回 |
空值处理 | 可为不存在的属性创建 ref | 必须存在于源对象 |
通过合理使用 toRef
和 toRefs
,可以更好地管理 Vue 3 中的响应式数据,确保数据的响应性在复杂的组件逻辑中不被破坏。
5. 工具型 Ref API
import { isRef, unref } from 'vue'
// 类型检查
if (isRef(someVar)) { /*...*/ }
// 安全获取值(如果是 ref 返回 .value,否则返回本身)
const rawValue = unref(possibleRef)
实战对比:选择正确的 Ref
场景 1:表单对象管理
// 推荐:ref (需要深度响应)
const form = ref({
user: {
name: '',
address: {
city: ''
}
}
})
场景 2:大型静态配置
// 推荐:shallowRef (性能优化)
const appConfig = shallowRef({
// 上千项配置,初始化后很少修改
})
// 修改时整体替换
appConfig.value = { ...newConfig }
场景 3:从 Props 提取属性
// 推荐:toRef (保持响应链接)
const props = defineProps(['title'])
const titleRef = toRef(props, 'title')
场景 4:自定义响应逻辑
// 推荐:customRef (高级控制)
const delayedSearch = useDebouncedRef('', 500)
性能优化技巧
1. 大型列表渲染
// 使用 shallowRef 包裹大数组
const bigList = shallowRef([...Array(10000).keys()])
2.避免不必要的深度响应
// 不需要深度响应时
const staticConfig = shallowRef({ ... })
3.组合式函数返回值
function useFeature() {
const data = ref(null) // 需要响应式
const utils = shallowRef({ // 不需要深度响应
helper1: () => {...},
helper2: () => {...}
})
return { data, utils }
}
utils工具函数为什么不直接用普通对象?
如果直接返回普通对象:
function useFeature() {
const utils = { helper1: () => {...} } // 普通对象
return { utils }
}
问题:当在模板中使用 utils.helper1()
时,Vue 会抛警告(非响应式值在模板中使用)
解决方案:
-
用
shallowRef
包裹(推荐) -
用
markRaw
显式标记:
import { markRaw } from 'vue'
const utils = markRaw({ helper1: () => {...} })
常见问题解答
Q1:什么时候该用 ref 而不是 reactive?
A:当需要处理基本类型、需要引用替换、或在组合式函数中返回时优先使用 ref。
Q2:toRef 和 toRefs 会创建新的响应式对象吗?
A:不会,它们只是创建对源对象属性的响应式引用,修改会直接影响源对象。
Q3:为什么有时候需要 triggerRef?
A:当使用 shallowRef 且需要手动触发更新时,或者实现自定义响应逻辑时使用。
Q4:如何选择 ref 和 shallowRef?
A:根据是否需要深度响应决定,对大型对象或性能敏感场景优先考虑 shallowRef。
总结
Vue 3 的 Ref 系统提供了不同粒度的响应式控制能力,从标准 ref
的便捷到 shallowRef
的性能优化,再到 customRef
的完全自定义,满足了各种复杂场景的需求。理解它们的核心差异,能够帮助开发者在保证功能的同时,写出性能更优的 Vue 代码。
记住选择原则:
-
默认用
ref
- 满足大多数需求 -
大对象用
shallowRef
- 性能优化 -
特殊逻辑
customRef
- 高级控制 -
解构属性用
toRef(s)
- 保持响应性
掌握这些 Ref 的使用技巧,你将能够更加游刃有余地处理 Vue 3 的响应式编程。