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,别反过来。



