在 Vue 3 的响应式系统中,shallowRef 和 shallowReactive 提供了“浅层响应式”(Shallow Reactivity)的能力。与默认的深层响应式不同,它们仅对对象的第一层属性进行响应式处理,而忽略嵌套结构。这种设计看似削弱了响应式的“智能”,实则是在特定场景下实现性能优化的关键工具。本节将深入探讨浅层响应式的原理,并结合真实应用场景,阐明何时应选择“放过”深层数据。
一、浅层 vs 深层:响应式代理的范围差异
要理解浅层响应式,必须首先明确其与 ref 和 reactive 在代理范围上的根本区别。
1. reactive:递归深层代理
reactive 使用 Proxy 包装对象,并在 get 拦截器中对返回的每个嵌套对象递归调用 reactive:
const get = (target, key, receiver) => {
const res = Reflect.get(target, key, receiver);
if (isObject(res)) {
return reactive(res); // 递归创建响应式代理
}
track(target, key);
return res;
};这意味着,无论数据嵌套多深,修改任何层级的属性都能触发依赖更新。但代价是:每次访问嵌套对象时,都可能创建新的 Proxy 实例,带来额外的内存和性能开销。
2. shallowReactive:仅代理第一层
shallowReactive 的实现则完全不同。它创建的 Proxy 只拦截第一层属性的读写,对嵌套对象不做任何处理:
const shallowGet = (target, key, receiver) => {
track(target, key);
return Reflect.get(target, key, receiver); // 直接返回,不递归
};
const shallowSet = (target, key, value, receiver) => {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
};在此模式下,只有直接修改根对象的属性(如 state.count 或 state.user)才会触发更新。如果 state.user 是一个普通对象,修改 state.user.name 不会触发视图更新,因为 user 对象本身不是响应式的。
3. ref 与 shallowRef:值的包裹深度
ref 会对 .value 中的对象自动进行深层响应式处理:
const obj = ref({ nested: { count: 0 } });
obj.value.nested.count++; // 触发更新,nested 被 reactive 处理而 shallowRef 仅将 .value 本身作为响应式引用,不对内部对象进行转换:
const obj = shallowRef({ nested: { count: 0 } });
obj.value.nested.count++; // 不会触发更新
obj.value = { nested: { count: 1 } }; // 手动替换整个值,才会触发更新二、性能优化的核心:避免不必要的代理开销
深层响应式的最大成本在于 代理对象的创建和内存占用。每一个被 reactive 包装的对象都会生成一个 Proxy 实例,这需要额外的内存空间。在大型应用中,如果状态树非常复杂,成千上万个 Proxy 实例会显著增加内存消耗。
此外,频繁访问深层嵌套对象会反复触发 reactive 的递归调用,影响运行时性能。
shallowReactive 和 shallowRef 通过限制代理范围,从根本上避免了这些开销。它们适用于那些不需要深层响应性或由外部状态管理的数据。
三、真实应用场景分析
场景一:大型不可变数据结构(Immutable Data)
当你的组件需要渲染一个大型的、从外部 API 获取的 JSON 数据(如树形目录、地理信息等),且这些数据在组件内部不会被修改时,使用 shallowReactive 或 shallowRef 是理想选择。
import { shallowRef, onMounted } from 'vue';
export default {
setup() {
const treeData = shallowRef(null); // 数据本身不需要响应式
onMounted(async () => {
const data = await fetch('/api/tree').then(r => r.json());
treeData.value = data; // 替换整个值
});
return { treeData };
}
}在此场景中:
- 数据量大,深层代理会浪费大量内存。
- 数据是“只读”的,不需要监听嵌套属性的变化。
- 更新时通常是替换整个数据集(如重新请求 API),而非局部修改。
使用 shallowRef 可以确保 .value 的替换能触发视图更新,同时避免对庞大 JSON 树的递归代理。
场景二:第三方库实例或 DOM 对象
第三方库的实例(如 echarts 实例、地图引擎实例)或 DOM 元素通常包含复杂的内部结构,且不应被 Vue 的响应式系统侵入。
import { shallowRef, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
export default {
setup() {
const chartInstance = shallowRef(null);
onMounted(() => {
const el = document.getElementById('chart');
chartInstance.value = echarts.init(el); // 存储实例
});
onUnmounted(() => {
chartInstance.value?.dispose();
});
return { chartInstance };
}
}使用 shallowRef 的原因:
- 避免 Vue 尝试代理第三方库复杂的原型链和内部属性,可能导致意外行为或性能问题。
- 我们只关心
chartInstance引用本身的赋值和销毁,不关心其内部属性是否变化。
场景三:性能敏感的列表渲染
在渲染大型列表时,如果每个列表项都是深层响应式对象,滚动或交互时可能会有明显卡顿。
import { shallowRef, computed } from 'vue';
export default {
setup() {
const items = shallowRef([]);
// 通过 computed 重建响应式连接
const displayedItems = computed(() =>
items.value.map(item => reactive(item))
);
return { displayedItems };
}
}这里采用了一种混合策略:
- 原始数据
items使用shallowRef存储,避免深层代理。 - 在
computed中按需将每个item转换为reactive,仅对当前显示的项建立响应式。
这种方法可以显著降低初始渲染的开销,尤其适合虚拟滚动等场景。
场景四:跨 Composition API 边界的性能优化
当多个自定义 Hook 共享一个大型状态对象时,若每次都传递深层响应式对象,可能会导致不必要的依赖追踪。
function useExpensiveState() {
const state = shallowReactive({
config: { /* ... */ },
cache: new Map(),
largeDataset: [] // 大型数据,无需深层响应式
});
return state;
}
function useUI(state) {
// 仅对 UI 相关的部分创建深层响应式
const uiState = reactive({
visible: true,
selectedId: null
});
return { ...toRefs(uiState), config: toRef(state, 'config') };
}通过 shallowReactive,我们确保共享状态的“骨架”是响应式的,但避免对其大型字段进行代理,从而提升整体性能。
四、与 markRaw 的对比
Vue 还提供了 markRaw API,用于完全跳过响应式处理。与 shallowReactive 的区别在于:
markRaw:标记对象及其所有子对象永不被代理。shallowReactive:对象本身是Proxy,只是不递归代理子对象。
如果你有一个绝对不应该被响应式化的对象(如类实例),应使用 markRaw。如果需要第一层属性的响应式,但希望跳过深层,则使用 shallowReactive。
五、总结
shallowRef 和 shallowReactive 并非“残缺版”的响应式 API,而是针对特定性能瓶颈的精准优化工具。
选择浅层响应式的时机包括:
- 数据结构庞大且主要作为只读展示。
- 对象包含不适合代理的复杂内部状态(如第三方库实例)。
- 需要精细控制响应式范围,避免过度追踪。
- 性能敏感场景,需减少内存占用和代理开销。
在实际开发中,应优先使用 ref 和 reactive,因为它们提供了最直观的开发体验。当遇到性能瓶颈或特殊需求时,再考虑引入浅层响应式。理解何时“放过”深层数据,是构建高性能 Vue 应用的重要技能。