陌上人如玉
公子世无双

shallowRef vs ref:区别、场景、实战

本文于 2026-07-26 20:32 更新,部分内容具有时效性,如有失效,请留言

shallowRef vs ref:区别、场景、实战

一句话先记住:
ref:深度响应式——改深层属性也触发更新
shallowRef:浅层响应式——只有 .value 整体替换才触发更新


先看现象

<script setup>
const user = ref({
  name: '张三',
  address: {
    city: '北京',
    detail: '朝阳区'
  }
})

const userShallow = shallowRef({
  name: '李四',
  address: {
    city: '上海',
    detail: '浦东新区'
  }
})

// 1. 改深层属性
user.value.address.city = '广州'        // ✅ 触发更新,页面变了
userShallow.value.address.city = '深圳' // ❌ 不触发更新,页面不变

// 2. 整体替换
user.value = { name: '王五', address: { city: '杭州', detail: '' } }         // ✅ 触发更新
userShallow.value = { name: '赵六', address: { city: '南京', detail: '' } }  // ✅ 触发更新

关键区别ref 经过 reactive 代理,深层对象的每个属性都是响应式的;shallowRef 只代理 .value 这一层,里面的对象是原样的。


底层原理

ref 的源码逻辑(简化版)

function ref(value) {
  // 判断 value 是不是对象
  // 如果是对象 → 交给 reactive 处理
  // 如果不是 → 用 get/set 劫持 .value
  return {
    get value() {
      track(this, 'value')    // 收集依赖
      return isObject(value) ? reactive(value) : value
      //                     ⬆ 关键:每次 get 都返回 reactive(value)
    },
    set value(newVal) {
      value = newVal
      trigger(this, 'value')  // 触发更新
    }
  }
}

shallowRef 的源码逻辑(简化版)

function shallowRef(value) {
  return {
    get value() {
      track(this, 'value')
      return value  // 直接返回原对象,不包 reactive
    },
    set value(newVal) {
      value = newVal
      trigger(this, 'value')
    }
  }
}

核心差异只有一行代码ref 在 get 时调用了 reactive(value)shallowRef 没有。

这就意味着:

ref.value 返回的是一个 Proxy 对象
  → 你改这个 Proxy 的任何深层属性,都会触发 setter → 通知更新

shallowRef.value 返回的是原始对象
  → 你改这个对象的属性,JS 不会通知任何人
  → 只有 shallowRef.value = xxx 整体赋值,才会被 set 拦截到 → 通知更新

什么时候用 shallowRef?

场景 1:大列表数据

// ❌ 用 ref 的问题
const products = ref<Product[]>([])

// 接口返回 10000 条商品
products.value = await fetchProducts()
// 此时 reactive 会给每一条商品、每个属性都创建 Proxy
// 10000 条 × 每个对象 5 个属性 = 50000 个 Proxy
// 光创建 Proxy 就要花几十毫秒,内存多占几 MB

// 后续你改 products.value[0].name = 'xxx',确实会触发更新
// 但你根本不会这么用——列表数据通常是整体替换,不是改某一条
// ✅ 用 shallowRef
const products = shallowRef<Product[]>([])

// 接口返回 10000 条商品
products.value = await fetchProducts()
// 只对 products.value 这一层做响应式
// 里面的 10000 条对象都是普通对象,没有 Proxy 开销
// 性能好很多

// 后续更新:整体替换
products.value = [...products.value, newProduct]
// ✅ 触发更新

// 如果非要改某一条
const newList = [...products.value]
newList[0] = { ...newList[0], name: '新名字' }
products.value = newList
// ✅ 整体替换后触发更新

性能差异

10000 条数据:
  ref:       创建 ~50000 个 Proxy,耗时 ~80ms,额外内存 ~10MB
  shallowRef:创建 0 个 Proxy,耗时 ~1ms,额外内存 ~0MB

场景 2:不需要深层响应式的对象

// 场景:图表配置对象
// 这个对象只在初始化时设置一次,后续不会修改内部属性
// 整个替换时才会触发更新

const chartOptions = shallowRef({
  title: { text: '销售趋势', style: { fontSize: 16 } },
  xAxis: { data: ['一月', '二月', '三月'] },
  yAxis: { type: 'value' },
  series: [{ data: [100, 200, 150], type: 'line' }]
})

// 初始化时设置
chartOptions.value = newOptions  // ✅ 触发更新

// 但你不会这样用:
// chartOptions.value.title.text = '新标题'  // ❌ 不会触发更新,但你要这么用也合理

// 如果确实要改深层属性:
chartOptions.value = {
  ...chartOptions.value,
  title: { ...chartOptions.value.title, text: '新标题' }
}
// ✅ 整体替换,触发更新

场景 3:与第三方库配合(非响应式数据)

// 场景:接入地图 SDK
// 地图实例是一个复杂对象,不需要 Vue 帮它做响应式

const mapInstance = shallowRef<AMap.Map | null>(null)

onMounted(() => {
  // 创建地图实例,只存引用,不需要响应式
  mapInstance.value = new AMap.Map('container', {
    zoom: 15,
    center: [116.39, 39.9]
  })
})

// 后续操作地图时,直接调实例方法
function flyTo(lng: number, lat: number) {
  mapInstance.value?.setCenter([lng, lat])
  // 这里不需要触发 Vue 的更新
  // 地图自己的渲染引擎会处理
}

如果这里用 ref,Vue 会给地图实例对象做响应式代理——完全没有意义,白白消耗性能。

场景 4:大量计算结果的缓存

// 场景:前端有一批数据需要做复杂计算
// 计算结果是一个大对象,但只关心"整体替换"

const computedResult = shallowRef<AnalysisResult | null>(null)

async function runAnalysis() {
  const rawData = await fetchData()

  // 复杂计算(耗时操作)
  const result = heavyCalculation(rawData)

  // 存结果——整体赋值
  computedResult.value = result
  // ✅ 触发更新

  // 后续不会去改 result 内部的某个字段
  // 如果要重新计算,也是整体替换
}

什么时候用 ref?

场景 1:表单数据

// 表单需要深层响应式——每个输入框的变化都要实时响应

const form = ref({
  name: '',
  age: 0,
  address: {
    province: '',
    city: '',
    detail: ''
  }
})

// 用户输入时,深层修改需要触发更新
form.value.name = inputName       // ✅ 需要触发更新
form.value.address.city = '北京'  // ✅ 需要触发更新

// 如果这里用 shallowRef:
// form.value.name = inputName           // ❌ 不会触发,页面不更新
// form.value = { ...form.value, name: inputName }  // 要这样写,太麻烦

场景 2:需要 watch 深层变化

const user = ref({ name: '张三', settings: { theme: 'dark' } })

watch(user, (newVal) => {
  // 改 user.value.name 或 user.value.settings.theme 都会触发
  console.log('user 变了')
}, { deep: true })
// ⚠️ watch ref 默认 deep: false,需要显式写 deep: true

// 如果用 shallowRef + watch:
const userShallow = shallowRef({ name: '张三', settings: { theme: 'dark' } })

watch(userShallow, (newVal) => {
  // 只监听 .value 整体替换
  // 改深层属性不会触发
})

场景 3:组件 v-model 绑定的复杂对象

<!-- 子组件 -->
<script setup>
const props = defineProps<{ config: Config }>()
const emit = defineEmits<{ 'update:config': [value: Config] }>()

// 用户操作后,需要通知父组件
function updateTheme(theme: string) {
  emit('update:config', {
    ...props.config,
    theme
  })
  // 这里一般不走 ref,直接用 props + emit
}
</script>

<!-- 父组件 -->
<template>
  <ConfigPanel v-model:config="pageConfig" />
</template>

<script setup>
// 父组件用 ref,方便深层监听变化
const pageConfig = ref({
  layout: 'grid',
  theme: 'light',
  columns: 3
})
</script>

对比总表

维度 ref shallowRef
响应式深度 深度(递归 reactive) 浅层(只有 .value)
value.a.b = 1 ✅ 触发更新 ❌ 不触发
value = { ... } 整体替换 ✅ 触发更新 ✅ 触发更新
内部对象是否被 Proxy 包裹 ✅ 是 ❌ 否
创建开销 大(递归遍历所有属性) 小(几乎没有)
内存占用 高(每个属性一个 Proxy) 低(原始对象)
适合场景 表单、需要深层监听的数据 大列表、第三方实例、计算结果
不适合场景 超大列表(性能差) 需要深层响应式的数据
watch 监听 默认只监听 .value 引用变化 默认只监听 .value 引用变化
watch + deep:true ✅ 监听所有深层变化 ⚠️ 不推荐,因为深层不是响应式的

面试官可能追问的问题

Q1:shallowRef 比 ref 快多少?

取决于数据量。10 条数据几乎没区别。10000 条数据,创建阶段 ref 比 shallowRef 慢 50-100ms,内存多占 5-10MB。运行时如果频繁读属性,ref 每次 get 都会经过 Proxy 拦截,有微小开销;shallowRef 直接返回原始值,没有额外开销。

Q2:shallowRef 的值改了深层属性,怎么让页面更新?

两种方式:
1. 整体替换:shallowRef.value = { ...shallowRef.value, deepProp: newVal }
2. 用 triggerRef(shallowRef) 手动触发更新

const data = shallowRef({ list: [1, 2, 3], info: { count: 0 } })

// 方式1:整体替换
data.value = { ...data.value, info: { count: 100 } }

// 方式2:手动触发
data.value.info.count = 100
triggerRef(data)  // 强制触发依赖更新

triggerRef 是 Vue 3.3+ 提供的 API,在修改了 shallowRef 内部数据但又不想整体替换时使用。

Q3:为什么 Vue 3 不默认都用 shallowRef?

因为 90% 的场景下,我们需要深层响应式——尤其是表单、配置对象、组件 props。如果默认用 shallowRef,每次改深层属性都要整体替换或者手动 trigger,开发体验会很差。shallowRef 是优化工具,不是默认选择

Q4:shallowReactive 和 shallowRef 什么关系?

shallowReactive 是 reactive 的浅层版本,跟 shallowRef 类似,但:
shallowRef 只对 .value 做浅层响应
shallowReactive 对对象的第一层属性做浅层响应

const state = shallowReactive({
  name: '张三',
  address: { city: '北京' }
})

state.name = '李四'                 // ✅ 触发更新(第一层)
state.address.city = '上海'         // ❌ 不触发(第二层)
state.address = { city: '广州' }    // ✅ 触发更新(第一层)

实际项目中的选择策略

// 1. 新写一个组件,不确定用哪个
//    先用 ref,性能有问题再换 shallowRef
//    不要过早优化

// 2. 已知数据量很大(>5000 条)
//    直接用 shallowRef
//    更新时整体替换

// 3. 存第三方实例(地图、播放器、WebSocket)
//    直接用 shallowRef
//    这些对象不需要响应式

// 4. 表单数据、配置对象
//    用 ref
//    需要深层响应式

// 5. 计算结果缓存
//    用 shallowRef
//    只关心整体替换

踩坑记录

坑 1:用了 shallowRef 后改了深层属性,页面不更新,排查了半天

const items = shallowRef([{ id: 1, name: '商品A' }])

// 某处代码
items.value[0].name = '商品B'  // ❌ 页面没变
// 因为 items.value 还是同一个数组引用,set 没触发
// 解:items.value = [...items.value] 或 triggerRef(items)

坑 2:shallowRef + v-for 的 :key 问题

<template>
  <div v-for="item in products" :key="item.id">
    {{ item.name }}
  </div>
</template>

<script setup>
const products = shallowRef([...])

// 如果修改了某个 item 的属性
products.value[0].name = '新名字'
// ❌ 页面不更新,因为 products.value 引用没变
// 但 item.name 确实变了,如果你重新触发渲染,它会显示新值
// 问题是:你怎么触发重新渲染?

// 解:
// 方案A:整体替换
products.value = [...products.value]

// 方案B:triggerRef
products.value[0].name = '新名字'
triggerRef(products)
</script>

坑 3:误以为 shallowRef 完全不响应

const count = shallowRef(0)

// 这个是可以的,因为改的是 .value 本身
count.value = 1  // ✅ 触发更新

// shallowRef 对基本类型(number/string/boolean)的行为和 ref 完全一样
// 区别只在 value 是对象时才体现

总结

用 ref 的时机:
  · 90% 的场景
  · 表单、配置、需要深层监听
  · 你不在乎那点性能差异

用 shallowRef 的时机:
  · 10% 的场景
  · 大列表数据(>5000 条)
  · 第三方实例
  · 计算结果缓存
  · 你确实知道"为什么这里不需要深层响应式"

一句话原则:先用 ref,卡了再换 shallowRef,别反过来。
赞(0) 打赏
未经允许不得转载:陌上寒 » shallowRef vs ref:区别、场景、实战

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

微信扫一扫

支付宝扫一扫