vue中ref与reactive
Vue 3 的 ref 与 reactive:区别、实现,以及为什么更推荐 ref
1. 一句话结论
ref:适合原始类型或独立、细粒度的状态单元;类型推断明确、解构安全、追踪粒度更小,默认首选。reactive:适合管理对象/集合这类结构化状态;但解构会丢失响应性,watch默认非深度,需要正确的使用姿势配合。
2. 用法与语义差异
2.1 基本示例
import { ref, reactive } from 'vue';
// ref:包装任何值(常见是原始类型)
const count = ref(0);
count.value++; // 必须通过 .value
// reactive:把对象/数组“代理化”
const state = reactive({ a: 1, nested: { b: 2 } });
state.a++; // 直接改
state.nested.b++; // 深层也响应(非 shallowReactive)
2.2 模板中的解包(unwrapping)
-
在 模板 里,
ref会自动解包,可直接写{{ count }},不必.value。 -
在响应式对象中(
reactive(obj)之后的对象),内部的ref属性会被解包为其.value(除shallowReactive):const r = reactive({ n: ref(1) }); console.log(r.n); // 1(已解包) r.n = 2; // 等价于给原 ref 赋值 2 -
数组或
Map/Set里的ref不会被自动解包(避免语义混乱)。
2.3 解构陷阱
-
reactive的解构/展开会失去响应性:const s = reactive({ a: 1 }); const { a } = s; // ❌ a 与原对象断联解决:用
toRefs:import { toRefs } from 'vue'; const { a } = toRefs(s); // ✅ a 是一个 ref -
ref解构安全:解构出来的仍是ref本身(带.value)。
2.4 watch 行为差别
import { watch } from 'vue';
watch(count, (v) => { /* 触发:count.value 改变 */ });
watch(state, (v) => { /* 不会因为 state.a 改变而触发(非深度)*/ });
watch(() => state.a, (v) => { /* 触发:state.a 改变 */ });
watch(state, (v) => { /* 深度监听 */ }, { deep: true }); // 成本更高
- 对
reactive根对象做watch,默认非深度。更推荐侦听具体字段或使用toRef/toRefs。
3. 内部实现原理(精简但够用)
3.1 总览:依赖收集与触发
Vue 3 基于 effect/依赖图 + track/trigger:
- track(target, key):在读取阶段收集“谁用到了哪个 key”。
- trigger(target, key):在写入阶段触发与该 key 相关的副作用。
3.2 reactive 的实现要点(Proxy)
-
核心是 ES Proxy 包装对象,对
get/set等拦截:const proxy = new Proxy(target, { get(target, key, receiver) { track(target, key); // 读取时收集依赖 const res = Reflect.get(target, key, receiver); // 深层对象懒代理(返回子对象时再做代理) return isObject(res) ? reactive(res) : res; }, set(target, key, value, receiver) { const old = (target as any)[key]; const ok = Reflect.set(target, key, value, receiver); if (ok && value !== old) trigger(target, key); // 定向触发 return ok; } }); -
优点:天然支持深层响应、对对象/数组的操作都能被追踪。
-
成本:读取任何属性都会
track,对象越大、层级越深,追踪图越复杂;深度监听(deep: true)开销明显。
3.3 ref 的实现要点(RefImpl)
-
本质是一个带 getter/setter 的壳:
class RefImpl<T> { private _value: T; dep = new Dep(); // 与这个 ref 绑定的依赖集合 get value() { trackRefValue(this); // 读取时收集依赖(以“这个 ref”为粒度) return this._value; } set value(next: T) { if (!Object.is(next, this._value)) { this._value = next; triggerRefValue(this); // 触发所有依赖这个 ref 的 effect } } } -
粒度更细:依赖挂在“这个
ref实例”上,而不是“某个对象上的 key”。对比reactive,依赖边更少、更可控。
4. 性能与可维护性对比(为什么更推荐 ref)
4.1 追踪粒度与稳定性
ref:每个 ref 一条独立依赖线,粒度最细;修改只触发依赖它的副作用。reactive:在对象级别做依赖图,容易“过度追踪”(读到多少 key 就 track 多少 key);深层对象的变动传播路径更复杂。
结果:小而独立的状态用 ref,渲染更可预期、无额外噪音更新。
4.2 类型推断与解构
ref<T>在 TS 中表现清晰:使用时总是.value;团队阅读/重构更直观。reactive解构会断开响应;需要toRefs,API 复杂度上升。
4.3 组合式复用
- 组合式函数(composables)对外暴露一组
ref,调用方易于挑选/重命名/解构,不担心丢响应。 - 库代码中暴露
ref也更**“自描述”**:这是“状态单元”,不是随意可变的大对象。
5. 什么时候仍然用 reactive?
-
模型型/表单型状态:天然是对象结构,字段多、需要一起传来传去。
-
需要深层响应且不想手动为每个字段造
ref。 -
集合/增删改频繁的对象/数组(如表格数据),配合
reactive([])或reactive(new Map())直观好用。小技巧:对数组的大改动,可用
arr.splice等原生方法,触发行为最可靠。
6. 常见 API 及坑位清单
6.1 相关 API
shallowRef/shallowReactive:只追踪顶层,不做深层代理/追踪。适合大对象(如第三方实例)或频繁整体替换的场景。toRef(obj, key)/toRefs(obj):从reactive对象生成可解构的 ref。unref(x)/isRef(x):工具化判断与剥壳。computed:产出只读ref(可set的 computed 是带 setter 的变体)。
6.2 常见坑
reactive解构丢响应 → 用toRefs。- 数组中的
ref不自动解包 → 渲染时item.value,或前置unref。 watch(reactiveObj)不触发 → 侦听具体 getter:watch(() => obj.a, ...),或加{ deep: true }(慎用)。- 从
props取值后解构 →const { title } = toRefs(props),保持响应。 - 与第三方不可代理对象(如 DOM、类实例) → 用
shallowRef存,整体替换。
7. 推荐的实践模式
7.1 默认使用 ref
import { ref, computed, watch } from 'vue';
// ✅ 小状态用 ref
const loading = ref(false);
const page = ref(1);
const keyword = ref('');
// ✅ computed / watch 都以 ref 为单位
const canSearch = computed(() => keyword.value.trim().length > 0);
watch(page, () => {
/* fetchList(page.value) */
});
7.2 需要对象时,用 reactive + toRefs 暴露
import { reactive, toRefs, computed } from 'vue';
function useForm() {
const form = reactive({
username: '',
password: '',
remember: false,
});
const valid = computed(() => form.username !== '' && form.password !== '');
return { ...toRefs(form), valid }; // 对外全是 ref,调用方解构安全
}
7.3 大对象/实例用 shallowRef
import { shallowRef, onMounted } from 'vue';
type Chart = any; // 替换为你的图表类型
const chart = shallowRef<Chart | null>(null);
onMounted(() => {
chart.value = /* new Chart(...) */ null;
});
function reset() {
chart.value?.destroy?.();
chart.value = null;
}
7.4 列表/表格数据(按需细化)
import { reactive } from 'vue';
// 行内交互状态细分为字段,避免整表刷新
const rows = reactive<{ id: number; checked: boolean }[]>([]);
// 或者把“是否选中”等独立成 Map<id, boolean>
const checkedMap = reactive(new Map<number, boolean>());
8. 更深入的实现差异(针对性能感知)
-
依赖键的维度
ref:以“实例”为键(单槽位依赖)。reactive:以“目标对象 + 属性 key” 为键(键数多时,依赖集合膨胀更快)。
-
读取时机
reactive.get每次读取属性都会track。ref.value只在读取.value时track,对模板/渲染更友好(模板会只读取使用到的那些ref)。
-
深层对象的惰性代理
reactive对子对象是惰性代理:访问到子对象时才reactive一次。但访问路径多时,仍会增加代理层与追踪点。
实战经验:能
ref就ref,能“按字段追踪”就别“整对象追踪深度变化”。
9. 选型清单(速查)
| 需求/场景 | 建议 |
|---|---|
| 原始类型、小状态 | ref |
| 字段很多的表单模型 | reactive + 导出 toRefs |
| 需要整体替换的大对象/第三方实例 | shallowRef |
| 深层结构频繁更新 | reactive(必要时局部字段拆成 ref) |
| 侦听变化 | watch(ref) 或 watch(() => obj.key),尽量避免 deep: true |
10. 小结:为什么更推荐 ref
- 更细的依赖粒度 → 更少无关更新、渲染更可控。
- 类型与语义更直观 →
.value明确表达“这是响应式单元”。 - 解构安全 → 组合式 API 中拆解/重命名无副作用。
- 更契合组合式设计 → composable 对外暴露
ref,复用边界清晰。
实践准则:默认用
ref,遇到结构化/深层状态再引入reactive,并用toRefs保持边界清晰。
11. 附:常用代码片段
11.1 从 reactive 派生出字段级 ref
import { reactive, toRef } from 'vue';
const user = reactive({ name: 'hua', age: 20 });
const name = toRef(user, 'name'); // ref<string>
name.value = '张三'; // 同步回 user.name
11.2 安全暴露组合式状态
import { ref } from 'vue';
export function useCounter() {
const n = ref(0);
const inc = () => n.value++;
const reset = () => (n.value = 0);
return { n, inc, reset }; // 外部拿到的都是 ref/函数
}
11.3 深层监听的替代写法
import { watch } from 'vue';
// 不用 deep: true,而是精确侦听需要的字段
watch(() => state.filters.keyword, (kw) => {
/* doSearch(kw) */
}, { /* 可配 debounce/throttle */ });如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录