shallowRef / shallowReactive 浅层响应式
Vue 3 默认响应式是深度的(
ref内部调用reactive、reactive递归代理嵌套对象)。当数据量巨大或持有第三方类实例时,深度代理成为性能瓶颈甚至导致行为异常。shallowRef和shallowReactive提供了"仅根级追踪"的逃生舱——你放弃深度响应式的便利,换来性能和对象身份的正确性。
速查卡片
| API | 入参 | 追踪层级 | 嵌套对象行为 | 需手动触发 | 典型场景 | 版本 |
|---|---|---|---|---|---|---|
shallowRef() | 任意类型 | 仅 .value | 存储原样,不代理 | triggerRef() | 大数据列表、第三方实例、不可变数据 | ≥ 3.0.0 |
shallowReactive() | 仅 object | 仅根级别属性 | 存储原样,不代理 | ❌(仅改引用) | 固定结构表单、配置对象 | ≥ 3.0.0 |
triggerRef() | ShallowRef | — | 手动触发 shallowRef 依赖更新 | — | 直接改了 shallowRef 深层但不想整体替换 | ≥ 3.0.0 |
概述
为什么需要"浅层"
Vue 3 的响应式引擎对每一次属性访问都会设置 Proxy 拦截,用于依赖追踪。当数据规模达到万级以上、或嵌套层级很深时,这层拦截的开销不可忽略。更严重的是,第三方类实例(如 ECharts 实例、Leaflet 地图对象、自定义 Class 含 private 字段)被深度代理后可能彻底失效——Proxy 会破坏 this 绑定、暴露私有字段导致不可预知的行为。
shallowRef 和 shallowReactive 的解决思路一致:只在顶层拦截,嵌套数据原样暴露。代价是你不能直接修改嵌套属性来触发更新——要么整体替换引用,要么调用 triggerRef 手动通知。
源码级机制
两者本质上复用同一套 Proxy 基础设施,只是传入不同的 Handler:
shallowRef(@vue/reactivity/src/ref.ts):
// ref() 内部:_value = toReactive(value) // 深度转换
// shallowRef() 内部:_value = value // 原样存储
function shallowRef(value?: unknown) {
return createRef(value, true) // isShallow = true
}RefImpl 构造函数中,isShallow=true 时跳过 toReactive() 调用,内层值直接存储,不经过 Proxy。
shallowReactive(@vue/reactivity/src/reactive.ts + baseHandlers.ts):
// reactive() → mutableHandlers(isShallow=false)
// shallowReactive() → shallowReactiveHandlers(isShallow=true)
// baseHandlers.ts 的 get 陷阱关键差异:
if (isShallow) {
return res // 直接返回原始值,不递归 reactive(res)
}
// 非 shallow 模式:
if (isObject(res)) {
return reactive(res) // 递归深度代理
}set 陷阱中 shallow 模式同样跳过 toRaw / isRef 检查,值原样写入。
shallowRef 详解
语法
import { shallowRef } from 'vue'
const state = shallowRef({ count: 1 })
// ❌ 不触发更新——深层属性修改不被追踪
state.value.count = 2
// ✅ 触发更新——整体替换 .value
state.value = { count: 2 }追踪机制
shallowRef 只追踪 .value 本身的读写。当你替换整个 .value 时,RefImpl.set value() 触发依赖更新;但修改 .value.xxx 只是操作一个未被代理的普通对象,不会有任何追踪。
这与 ref 形成鲜明对比:
const deep = ref({ a: { b: 1 } })
deep.value.a.b = 2 // ✅ 触发——内部 reactive 代理了所有层级
const shallow = shallowRef({ a: { b: 1 } })
shallow.value.a.b = 2 // ❌ 不触发——a 和 b 都没有被代理适用场景
| 场景 | 为什么用 shallowRef |
|---|---|
| 大数据列表(万级条目) | ref 会为每个条目创建 Proxy,内存和访问开销巨大 |
| 第三方类实例(ECharts、Leaflet、自定义 Class) | Proxy 破坏 this、private 字段、原型方法 |
| 不可变数据(Immutable.js、Redux 风格) | 每次更新都是替换整个引用,深层追踪毫无意义 |
| 组件对象 / DOM 引用 | ref 包装组件会触发 "made a reactive object" 警告 |
| 非 Vue 状态(外部 store、WebSocket 数据) | 外部系统自己管理更新,Vue 只需感知引用变化 |
shallowReactive 详解
语法
import { shallowReactive, isReactive } from 'vue'
const state = shallowReactive({
foo: 1,
nested: { bar: 2 }
})
// ✅ 触发更新——根级别属性
state.foo++
// ❌ 不触发更新——嵌套对象未被代理
state.nested.bar++
// 验证
isReactive(state.nested) // false —— 嵌套对象是裸的追踪机制
shallowReactive 为对象创建 Proxy,但 Proxy 的 get 陷阱在 isShallow=true 时不递归调用 reactive(res)。也就是说:
- 根属性
.foo—— 被追踪,修改触发更新 - 嵌套对象
.nested—— 访问时返回原始对象,修改其属性不触发更新 - 嵌套对象替换
state.nested = { bar: 3 }—— 修改根属性,触发更新
注意
官方明确警告:shallowReactive 只应该在组件根级状态中使用。将它嵌套进深度 reactive 对象会产生"响应式不一致"的混乱行为,极难调试:
const deep = reactive({
shallow: shallowReactive({ count: 0 })
})
// ❌ 不推荐:deep.shallow 的行为变得难以预测triggerRef 手动触发
语法
import { shallowRef, triggerRef } from 'vue'
function triggerRef(ref: ShallowRef): void用途
当你直接修改了 shallowRef 值的深层属性,但又不想整体替换引用时,可以用 triggerRef 手动通知 Vue 重新执行依赖该 ref 的 effect。
const list = shallowRef([1, 2, 3])
// 直接 push 不触发更新
list.value.push(4)
// 手动触发
triggerRef(list)源码原理
// @vue/reactivity/src/ref.ts
export function triggerRef(ref: Ref): void {
if ((ref as RefImpl).dep) {
(ref as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: (ref as RefImpl)._value,
})
}
}本质就是直接调用 ref 内部的 dep.trigger(),让所有依赖这个 ref 的 effect 重新执行。Vue 不会检查数据是否真的变了——你说触发,它就触发。
基础用法
示例一:最简——shallowRef 包裹大数组
<script setup lang="ts">
import { shallowRef } from 'vue'
interface Item { id: number; label: string }
const list = shallowRef<Item[]>([])
// 整体替换——触发更新
function loadData(items: Item[]) {
list.value = items
}
// 添加一项——也通过整体替换
function addItem(item: Item) {
list.value = [...list.value, item]
}
</script>
<template>
<ul>
<li v-for="item in list" :key="item.id">{{ item.label }}</li>
</ul>
</template>示例二:典型——第三方图表实例
<script setup lang="ts">
import { shallowRef, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts'
const chartRef = shallowRef<echarts.ECharts | null>(null)
onMounted(() => {
// shallowRef 确保 echarts 实例不被深度代理
chartRef.value = echarts.init(document.getElementById('chart')!)
chartRef.value.setOption({ /* ... */ })
})
onUnmounted(() => {
chartRef.value?.dispose()
})
</script>
<template>
<div id="chart" style="width: 600px; height: 400px" />
</template>如果用
ref,echarts 实例的数百个内部方法都会被 Proxy 包裹,轻则性能骤降,重则this绑定错乱导致图表无法交互。
示例三:进阶——shallowReactive 固定结构表单
<script setup lang="ts">
import { shallowReactive } from 'vue'
// 表单字段固定,嵌套对象不会被深度代理
const form = shallowReactive({
name: '',
preferences: {
theme: 'light',
language: 'zh-CN'
}
})
// ✅ 根属性修改——触发更新
function setName(val: string) {
form.name = val
}
// 嵌套对象只能整体替换
function setPreferences(prefs: typeof form.preferences) {
form.preferences = prefs
}
</script>
<template>
<input v-model="form.name" />
</template>示例四:冷门——triggerRef 批量 push
<script setup lang="ts">
import { shallowRef, triggerRef, watchEffect } from 'vue'
const items = shallowRef<number[]>([])
// 批量 push 后一次 triggerRef,而非每次 push 都整体替换
function batchAdd(nums: number[]) {
items.value.push(...nums) // push N 次,不触发任何更新
triggerRef(items) // 一次性通知,且避免了 N 次数组展开+替换
}
// 验证追踪
watchEffect(() => {
console.log('列表长度:', items.value.length)
})
</script>
<template>
<button @click="batchAdd([1, 2, 3, 4, 5])">批量添加 5 项</button>
<p>共 {{ items.length }} 项</p>
</template>对比:
items.value = [...items.value, ...nums]每次展开大数组都有 GC 压力。push + triggerRef零拷贝,适合高性能场景。
API 参考
shallowRef()
| 项 | 内容 |
|---|---|
| 签名 | function shallowRef<T>(value: T): ShallowRef<T> |
| 入参 | value: 任意类型的初始值 |
| 返回值 | ShallowRef<T>,仅 .value 级响应式 |
| 版本 | ≥ 3.0.0 |
shallowReactive()
| 项 | 内容 |
|---|---|
| 签名 | function shallowReactive<T extends object>(target: T): T |
| 入参 | target: 对象类型(普通对象、数组、Map、Set 等) |
| 返回值 | 仅根级属性响应式的 Proxy |
| 限制 | 仅接受对象,嵌套对象不代理。应仅在组件根级状态使用 |
| 版本 | ≥ 3.0.0 |
triggerRef()
| 项 | 内容 |
|---|---|
| 签名 | function triggerRef(ref: ShallowRef): void |
| 入参 | ref: 一个 ShallowRef 实例 |
| 副作用 | 强制触发所有依赖该 ref 的 effect 重新执行 |
| 版本 | ≥ 3.0.0 |
类型定义
// @vue/reactivity/src/ref.ts
declare const ShallowRefMarker: unique symbol
// ShallowRef 是 Ref 的子类型,带 ShallowRefMarker 标记
type ShallowRef<T = any, S = T> = Ref<T, S> & {
[ShallowRefMarker]?: true
}
// shallowRef 类型重载
function shallowRef<T>(value: T): ShallowRef<T>
function shallowRef<T = any>(): ShallowRef<T | undefined>
// triggerRef
function triggerRef(ref: Ref): void
// @vue/reactivity/src/reactive.ts
// shallowReactive 类型
function shallowReactive<T extends object>(target: T): T
// ShallowReactive 工具类型
type ShallowReactive<T> = T & { [ShallowReactiveBrand]?: true }ref / shallowRef 对比决策
| 维度 | ref() | shallowRef() |
|---|---|---|
.value.xxx 是否追踪 | ✅ 深度追踪 | ❌ 不追踪 |
| 内存开销 | 高(每层嵌套都创建 Proxy) | 低(仅 RefImpl 自身) |
| 属性访问性能 | Proxy 拦截每次读 | 直接读取(无拦截) |
| 第三方类实例 | ❌ 可能破坏行为 | ✅ 保留原始身份 |
| 修改方式 | 直接改属性 | 整体替换 .value 或 triggerRef() |
| 模板中使用 | obj.a.b 修改自动更新 | obj.a.b 修改不更新 |
| 维度 | reactive() | shallowReactive() |
|---|---|---|
| 嵌套对象是否代理 | ✅ 递归代理 | ❌ 不代理 |
| 根属性修改 | ✅ 追踪 | ✅ 追踪 |
| 嵌套属性修改 | ✅ 追踪 | ❌ 不追踪 |
| isReactive(nested) | true | false |
何时选 shallow
| 信号 | 行动 |
|---|---|
| 数据量大(> 1000 条,每条多个字段) | shallowRef + 整体替换 |
| 集成第三方库(ECharts、Leaflet、Three.js 等) | shallowRef 持有实例 |
| 不可变数据流(每次更新都整体替换) | shallowRef |
| 表单字段固定、嵌套配置不需追踪 | shallowReactive |
| 直接改了深层又想触发更新 | shallowRef + triggerRef |
| 数据量小、结构简单、频繁改嵌套属性 | 老老实实用 ref / reactive |
注意事项与边界情况
triggerRef 不检查数据变化
const s = shallowRef({ x: 1 })
triggerRef(s) // ✅ 触发更新,即使数据没变!triggerRef 只是粗暴地调用 dep.trigger(),它不管你改没改。意味着多余的 triggerRef 会造成无效重渲染。
shallowRef 依赖追踪的意外丢失
const s = shallowRef({ list: [] })
// computed 主动读取了 s.value.list
// 但 list 不是响应式的——computed 不会在 list 变化时重新计算
const length = computed(() => s.value.list.length)
s.value.list.push(1) // computed 不重新计算!
triggerRef(s) // 手动触发后重新计算shallowReactive 嵌套在 reactive 中的混乱
const outer = reactive({
inner: shallowReactive({ x: 0 })
})
outer.inner.x++ // ❌ shallowReactive 的嵌套属性不改追踪
outer.inner = shallowReactive({ x: 1 }) // ✅ 替换根属性触发这种嵌套极难调试——官方明确不推荐。
与 markRaw 的区别
markRaw 是标记某个对象永不被代理,shallowRef/shallowReactive 是只代理顶层。前者更极端——标记后的对象在任何 reactive 中都不会被深度转换。
import { markRaw, reactive, isReactive } from 'vue'
const obj = markRaw({ nested: { x: 1 } })
const state = reactive({ data: obj })
isReactive(state.data.nested) // false —— markRaw 阻止了代理常见误区
❌ shallowRef 的
.value赋值也会被深度代理 → 不会。shallowRef中_value = value,原样存储,不做toReactive。❌ shallowReactive 的嵌套对象改了也能触发更新 → 不能。只有根级别属性被代理,
state.nested.bar = 3不触发任何副作用。❌ triggerRef = 临时把 shallowRef 变成 ref → 不是。
triggerRef只是手动触发一次依赖通知,不会改变 ref 的追踪行为。下次再改深层属性,仍然需要再次triggerRef。❌ triggerRef 能触发 shallowReactive 的更新 → 不能。
triggerRef签名为(ref: ShallowRef): void,只接受 Ref。shallowReactive没有这个机制——要么改根属性,要么整体替换。❌ 性能场景没到 10000 条数据就可以无视 → 第三方实例才是更常见的理由。只要你持有 ECharts、地图 SDK、音视频播放器等第三方对象,就应该用
shallowRef。❌
shallowReactive和reactive只是"性能版本"的区别 → 不是。用法完全不同。reactive可以state.a.b.c = 1;shallowReactive只能state.a = newValue。混淆用法会导致"完全没反应的 bug"。
版本信息
| 版本 | 变更 |
|---|---|
| 3.0.0 | shallowRef()、shallowReactive()、triggerRef() 首次发布 |
| 3.2.0 | triggerRef() 增强,支持 ObjectRefImpl 实例 |
| 3.4.0 | 响应式系统重构,浅层追踪性能进一步提升 |
来源
| 来源 | 链接 |
|---|---|
| 官方 API — Reactivity Advanced | vuejs.org/api/reactivity-advanced |
| 官方指南 — 性能优化 | vuejs.org/guide/best-practices/performance |
| 源码 — ref.ts (shallowRef / triggerRef) | github.com/vuejs/core |
| 源码 — reactive.ts (shallowReactive) | github.com/vuejs/core |
| 源码 — baseHandlers.ts (shallow 代理机制) | github.com/vuejs/core |
最后整理:2026-07-10 · 对应 Vue 版本:3.5.x