Skip to content

shallowRef / shallowReactive 浅层响应式

Vue 3 默认响应式是深度的(ref 内部调用 reactivereactive 递归代理嵌套对象)。当数据量巨大或持有第三方类实例时,深度代理成为性能瓶颈甚至导致行为异常。shallowRefshallowReactive 提供了"仅根级追踪"的逃生舱——你放弃深度响应式的便利,换来性能和对象身份的正确性。

速查卡片

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 绑定、暴露私有字段导致不可预知的行为。

shallowRefshallowReactive 的解决思路一致:只在顶层拦截,嵌套数据原样暴露。代价是你不能直接修改嵌套属性来触发更新——要么整体替换引用,要么调用 triggerRef 手动通知。

源码级机制

两者本质上复用同一套 Proxy 基础设施,只是传入不同的 Handler:

shallowRef@vue/reactivity/src/ref.ts):

typescript
// 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):

typescript
// 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 详解

语法

typescript
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 形成鲜明对比:

typescript
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 详解

语法

typescript
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 对象会产生"响应式不一致"的混乱行为,极难调试:

typescript
const deep = reactive({
  shallow: shallowReactive({ count: 0 })
})
// ❌ 不推荐:deep.shallow 的行为变得难以预测

triggerRef 手动触发

语法

typescript
import { shallowRef, triggerRef } from 'vue'

function triggerRef(ref: ShallowRef): void

用途

当你直接修改了 shallowRef 值的深层属性,但又不想整体替换引用时,可以用 triggerRef 手动通知 Vue 重新执行依赖该 ref 的 effect。

typescript
const list = shallowRef([1, 2, 3])

// 直接 push 不触发更新
list.value.push(4)

// 手动触发
triggerRef(list)

源码原理

typescript
// @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 包裹大数组

vue
<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>

示例二:典型——第三方图表实例

vue
<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 固定结构表单

vue
<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

vue
<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

类型定义

typescript
// @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 拦截每次读直接读取(无拦截)
第三方类实例❌ 可能破坏行为✅ 保留原始身份
修改方式直接改属性整体替换 .valuetriggerRef()
模板中使用obj.a.b 修改自动更新obj.a.b 修改不更新
维度reactive()shallowReactive()
嵌套对象是否代理✅ 递归代理❌ 不代理
根属性修改✅ 追踪✅ 追踪
嵌套属性修改✅ 追踪❌ 不追踪
isReactive(nested)truefalse

何时选 shallow

信号行动
数据量大(> 1000 条,每条多个字段)shallowRef + 整体替换
集成第三方库(ECharts、Leaflet、Three.js 等)shallowRef 持有实例
不可变数据流(每次更新都整体替换)shallowRef
表单字段固定、嵌套配置不需追踪shallowReactive
直接改了深层又想触发更新shallowRef + triggerRef
数据量小、结构简单、频繁改嵌套属性老老实实用 ref / reactive

注意事项与边界情况

triggerRef 不检查数据变化

typescript
const s = shallowRef({ x: 1 })
triggerRef(s)  // ✅ 触发更新,即使数据没变!

triggerRef 只是粗暴地调用 dep.trigger(),它不管你改没改。意味着多余的 triggerRef 会造成无效重渲染

shallowRef 依赖追踪的意外丢失

typescript
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 中的混乱

typescript
const outer = reactive({
  inner: shallowReactive({ x: 0 })
})

outer.inner.x++  // ❌ shallowReactive 的嵌套属性不改追踪
outer.inner = shallowReactive({ x: 1 })  // ✅ 替换根属性触发

这种嵌套极难调试——官方明确不推荐。

与 markRaw 的区别

markRaw标记某个对象永不被代理shallowRef/shallowReactive只代理顶层。前者更极端——标记后的对象在任何 reactive 中都不会被深度转换。

typescript
import { markRaw, reactive, isReactive } from 'vue'

const obj = markRaw({ nested: { x: 1 } })
const state = reactive({ data: obj })
isReactive(state.data.nested)  // false —— markRaw 阻止了代理

常见误区

  1. shallowRef 的 .value 赋值也会被深度代理 → 不会。shallowRef_value = value,原样存储,不做 toReactive

  2. shallowReactive 的嵌套对象改了也能触发更新 → 不能。只有根级别属性被代理,state.nested.bar = 3 不触发任何副作用。

  3. triggerRef = 临时把 shallowRef 变成 ref → 不是。triggerRef 只是手动触发一次依赖通知,不会改变 ref 的追踪行为。下次再改深层属性,仍然需要再次 triggerRef

  4. triggerRef 能触发 shallowReactive 的更新 → 不能。triggerRef 签名为 (ref: ShallowRef): void,只接受 Ref。shallowReactive 没有这个机制——要么改根属性,要么整体替换。

  5. 性能场景没到 10000 条数据就可以无视 → 第三方实例才是更常见的理由。只要你持有 ECharts、地图 SDK、音视频播放器等第三方对象,就应该用 shallowRef

  6. shallowReactivereactive 只是"性能版本"的区别 → 不是。用法完全不同。reactive 可以 state.a.b.c = 1shallowReactive 只能 state.a = newValue。混淆用法会导致"完全没反应的 bug"。

版本信息

版本变更
3.0.0shallowRef()shallowReactive()triggerRef() 首次发布
3.2.0triggerRef() 增强,支持 ObjectRefImpl 实例
3.4.0响应式系统重构,浅层追踪性能进一步提升

来源

来源链接
官方 API — Reactivity Advancedvuejs.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