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  阅读(43)  评论(0)    收藏  举报