shallowRef 详解

一句话总结

ref().value深度响应式代理shallowRef() 只对 .value 本身做响应式追踪,不触及内部属性。


基本使用

import { ref, shallowRef } from 'vue';

// ─── ref — 深度响应式 ───
const state1 = ref({ count: 0, nested: { value: 'hello' } });
state1.value.count = 1;           // ✅ 触发更新(深层追踪)
state1.value.nested.value = 'hi'; // ✅ 触发更新(深层追踪)

// ─── shallowRef — 浅层响应式 ───
const state2 = shallowRef({ count: 0, nested: { value: 'hello' } });
state2.value.count = 2;           // ❌ 不会触发视图更新!
state2.value.nested.value = 'hi'; // ❌ 不会触发视图更新!
state2.value = { count: 2, nested: { value: 'hi' } }; // ✅ 触发更新(替换整个 .value)

核心区别: shallowRef 只有当你整体替换 .value 时才会触发响应式更新;修改 .value 内部的深层属性不会触发更新。


源码层面的差异

对比维度 ref() shallowRef()
内部逻辑 toReactive(value)reactive() 代理 跳过 toReactive,直接存原始值
getter 返回 reactive 代理对象,读取时 track 返回原始值,只对 .value 整体 track
setter 新值也转 reactive,然后 trigger 直接替换 .value,然后 trigger
性能开销 对象越大、嵌套越深,代理开销越大 几乎零开销,只追踪一个引用

伪代码:

// ref 内部
class RefImpl {
  get value() {
    track();
    return isObject(this._rawValue)
      ? reactive(this._rawValue)    // 🔴 深度代理
      : this._rawValue;
  }
  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = isObject(newVal) ? reactive(newVal) : newVal;
      trigger();
    }
  }
}

// shallowRef 内部
class ShallowRefImpl {
  get value() {
    track();
    return this._value;             // 🟢 直接返回,不做 reactive
  }
  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = newVal;         // 🟢 直接替换,不做代理
      trigger();
    }
  }
}

使用前后对比

❌ 使用 ref(问题)

// 第三方库实例(ECharts / 地图 / 编辑器)
const chartInstance = ref<ECharts | null>(null);

onMounted(() => {
  // 🔴 echarts.init() 返回的实例有 1000+ 内部属性
  //    赋值时 Vue 递归代理所有属性,造成卡顿
  chartInstance.value = echarts.init(container, options);
});

// 🔴 实例内部属性被意外追踪,类型退化为 UnwrapRef<ECharts>

问题:

  1. 性能开销 — Vue 递归 Proxy 整个实例,耗时随属性数量线性增长
  2. 内存浪费 — Proxy + 原始对象双份存储
  3. 类型退化UnwrapRef<ECharts> 丢失原始 API 的类型提示
  4. 意外追踪 — 库内部状态变化可能触发 Vue effect 重新收集

✅ 使用 shallowRef(正确)

const chartInstance = shallowRef<ECharts | null>(null);

onMounted(() => {
  // 🟢 直接存储原始 ECharts 实例,零代理开销
  chartInstance.value = echarts.init(container, options);
});

// 🟢 通过原生 API 操作(这些操作不影响 Vue 响应式)
chartInstance.value?.setOption({ ... });

// 🟢 需要触发 Vue 更新时才整体替换
function destroyAndRecreate() {
  chartInstance.value?.dispose();
  chartInstance.value = echarts.init(container, newOptions); // 触发更新
}

典型应用场景

场景 示例 为什么用 shallowRef
第三方库实例 ECharts、Leaflet、Monaco Editor、Three.js、AMap 实例内部状态由库自己管理,Vue 不需要代理
大型不可变数据 API 返回的千行级表格数据 只需整体替换数据源,不关心单元格级变更
模板 DOM 引用 v-for 中的 template ref 数组 DOM 元素不需要深度响应式
WebSocket / EventSource 连接实例 由浏览器管理,只需保存引用
Worker 实例 Web Worker、Service Worker 同上
外部状态管理 XState 等第三方状态机 状态机自己管理转换,Vue 只需知道"哪个状态"

triggerRef — 手动触发更新

import { shallowRef, triggerRef } from 'vue';

const state = shallowRef({ count: 0 });

state.value.count++;  // ❌ 不会触发视图更新

triggerRef(state);    // 🔔 手动通知 Vue:shallowRef 的内部变了,请更新视图

triggerRef 是"逃生舱",不应该作为常规手段使用。如果需要频繁手动触发,说明你应该用 ref() 而非 shallowRef()


isReactive — 检测是否被代理

import { ref, shallowRef, isReactive } from 'vue';

const r = ref({ a: 1 });
const s = shallowRef({ a: 1 });

console.log(isReactive(r.value)); // true  — 被深度代理
console.log(isReactive(s.value)); // false — 原始对象

常见误区

// ⚠️ 误区 1:shallowRef 内部属性修改后不会自动更新
const list = shallowRef([1, 2, 3]);
list.value.push(4);        // ❌ 不会触发更新!
list.value = [...list.value, 4]; // ✅ 整体替换

// ⚠️ 误区 2:shallowRef 和 ref 混用时模板行为不同
const s = shallowRef({ count: 0 });
const r = ref({ count: 0 });
// 模板中 {{ s.count }} {{ r.count }}
// s.value.count++  → s.count 模板不更新
// r.value.count++  → r.count 模板更新

// ⚠️ 误区 3:用 triggerRef 代替整体替换
const s = shallowRef({ count: 0 });
s.value.count++;
triggerRef(s); // 能用,但说明你选错了 API — 应该用 ref()

什么情况下会"替换整个对象"?

这个问题触及 shallowRef 两种截然不同的使用模式:

模式 A:第三方实例 — 几乎不替换,只需要"持有引用"

ECharts、地图、编辑器等实例,创建后通过库自己的 API 修改内部状态,不需要 Vue 追踪:

const instance = shallowRef<ECharts | null>(null);

// 创建 — 只做一次
instance.value = echarts.init(dom);

// 修改 — 通过库原生 API,不触发 Vue 更新
instance.value.setOption({ ... });

// 替换 — 只在销毁重建时发生
instance.value?.dispose();
instance.value = echarts.init(dom);  // 极少触发

这类场景的核心诉求不是"频繁替换触发更新",而是"别动我的实例,我只是找个地方存引用"。用 ref() 反而会深度代理实例造成性能浪费。

模式 B:不可变数据 — 每次都替换,天然匹配

API 数据、配置对象等,每次变更都是全新的快照

const list = shallowRef<Item[]>([]);
const config = shallowRef<Config>(defaultConfig);

// API 翻页 — 整个数组替换
list.value = await fetchPage(2);           // ✅ 触发更新

// 恢复快照 — 整个对象替换
config.value = historyStack.pop();         // ✅ 触发更新

// 表单重置 — 整个对象替换
config.value = { ...defaultConfig };       // ✅ 触发更新

这类场景的数据流本身就是"整体替换",天然匹配 shallowRef 的触发机制。

一句话区分

替换频率 触发更新的诉求
模式 A(实例引用) 极低,几乎只在销毁重建时 "别追踪内部就行"
模式 B(不可变数据) 每次变更都替换 "替换时通知 Vue"

两种模式的共同点:都不需要 Vue 追踪对象内部属性的变化


决策口诀

能用 ref() 就用 ref(),只有当你明确知道某个值是"黑盒"(第三方实例、巨型不可变数据)且不需要深层追踪时,才换 shallowRef()

  • ref() 是默认选择,安全且正确
  • shallowRef() 是性能优化手段,不是默认选项
  • 如果你发现自己频繁用 triggerRef(),说明应该换回 ref()
posted @ 2026-06-16 13:56  HuangBingQuan  阅读(19)  评论(0)    收藏  举报