理解笔记
首先需要理解watch和deep的重要概念,详见【Vue巨坑】当 watch 和 deep: true 相遇:一场由“newVal === oldVal”引发的血案
ref和reactive不一样
- 不设置deep的情况下(默认
deep: false,但reactive不太一样是特例),在watch的第一个参数中直接放一个ref,只能监听到第一层的变化(如ref.value的变化),修改ref.value.a或ref.value.a.b不会触发回调执行;而放一个reactive则能监听到任何一层,是深度的,默认效果和设置了deep: true是一样的,如果显式设置了deep: false,那么reactive的深层也监听不到了,只能监听到reactive.a的变化。 - 在设置
deep: true的情况下ref和reactive效果一样,都能监听到深层的变化(如xx.a.b的变化) - 容易出现误区的地方是(重点理解):
- 官网文档说如果给ref赋一个对象作为值,那么会给这个对象包一层reactive来实现深层响应性。所以把
ref.value放到watch监听中的效果等同于放了一个reactive。但在<template></template>中,ref.a.b(其实ref.a同理)也能响应式更新(在watch中,监听ref无法监听到ref.a.b的变化,但其实这个时候相当于设置了() => ref.value.a.b,即在template中使用ref.xx.xx默认等效于watch中的() => ref.value.xx.xx)。 - 假如说声明一个
const obj = ref({ a: { b: 1 } }),那么在不设置deep的情况下,在watch的第一个参数中放obj,那么只能监听到obj.value,无法监听到obj.value.a和obj.value.a.b的变化;放obj.value则可以监听到obj.value.a和obj.value.a.b的变化。在<template></template中直接写obj.a.b却可以响应式更新。(本质是对于ref而言,只能监听到ref.value的值的变化;而reactive默认深层监听,可以监听到reactive.a.b)
- 官网文档说如果给ref赋一个对象作为值,那么会给这个对象包一层reactive来实现深层响应性。所以把
举例
如下demo片段,obj.a.b和obj.c在<template></template>中都能响应式更新
<script setup>
import { ref } from 'vue'
const obj = ref({
a: {
b: 1
},
c: 1
})
setInterval(() => {
obj.value.a.b++;
// obj.value.c++;
}, 300);
</script>
<template>
<h1>{{ obj.a.b }}</h1>
<!-- <h1>{{ obj.c }}</h1> -->
</template>
另一个例子,需要重点理解一个易错点是:
watch中监听ref(未设置deep时),改变ref.value.a是不会触发回调执行的!!!只有改变ref.value才会触发,即使ref.value是个reactive对象。
<script setup>
import { ref, watch } from 'vue'
const obj = ref({
a: {
b: 1
},
c: 1
});
const numA = ref(0);
const numB = ref(0);
setInterval(() => {
// obj.value.a.b++;
obj.value.c++;
}, 300);
// obj 是个 ref, 默认效果是 deep: false, 只能监听到 ref.value 本身的变化
// [易错点!!!] 即只有给 ref.value = {} 一个新地址的时候才能触发回调执行
watch(obj, () => {
numA.value = numA.value + 1;
});
// obj.value 是个 reactive, 默认深层, 除非显式设置 deep: false
watch(obj.value, () => {
numB.value = numB.value + 1;
});
</script>
<template>
<h1>{{ obj.a.b }}</h1>
<!-- <h1>{{ obj.c }}</h1> -->
<div>numA不会变: {{ numA }}</div>
<div>numB会变: {{ numB }}</div>
</template>
PS:有时将watch监听的依赖项用computed包裹一下能避免意外的回调触发。
【2025.10.17补充】shallowRef和shallowReactive也有很大区别
- 用
ref时会给xxx.value的值包一层Proxy(也会递归对象的属性给每一层的对象都包一层Proxy),但是用shallowRef时不会包这一层Proxy - 用
reactive或shallowReactive时会直接给值包一层Proxy,区别是reactive还会递归对象的属性,给所有的对象属性也包一层Proxy` - 【坑】用
<el-table>时如果给表格传递的data是shallowRef(如shallowRefData),那么在push或splice这个shallowRef数据时,即使用了triggerRef(shallowRefData)也不会触发表格视图的更新(算是Element Plus表格实现的一点缺陷吧。。没考虑到适配这种场景);这个时候可以改成用shallowReactive,视图依然可以刷新。
关于shallowRef和shallowReactive的AI小结
✅ 你这段总结 非常接近正确且到位,我来帮你更系统地确认与补充几个关键细节。
✅ 一、关于 ref / shallowRef 的区别
| 特性 | ref |
shallowRef |
|---|---|---|
| 包装层 | 对 .value 的值做深层 Proxy |
.value 原样保存,不再做 Proxy |
| 深层响应性 | 递归所有嵌套对象(深层) | 不递归(浅层) |
| 触发更新 | Vue 会追踪 .value 的 getter / setter |
Vue 只追踪 .value 的替换(不会追踪其内部字段变化) |
例子:
const obj = ref({ name: 'A', info: { city: 'Tokyo' } })
obj.value.info.city = 'Kyoto' // ✅ 会触发更新(深层追踪)
const shallow = shallowRef({ name: 'A', info: { city: 'Tokyo' } })
shallow.value.info.city = 'Kyoto' // ❌ 不触发更新(内部未被代理)
✅ 二、关于 reactive / shallowReactive 的区别
| 特性 | reactive |
shallowReactive |
|---|---|---|
| 包装层 | 整个对象及其所有嵌套属性都变为 Proxy | 仅最外层对象变为 Proxy |
| 深层追踪 | 深度递归 | 不递归 |
| 顶层属性变化 | ✅ 会触发更新 | ✅ 会触发更新 |
| 深层属性变化 | ✅ 会触发更新 | ❌ 不会触发更新 |
例子:
const deep = reactive({ user: { name: 'Alice' } })
deep.user.name = 'Bob' // ✅ 响应式更新
const shallow = shallowReactive({ user: { name: 'Alice' } })
shallow.user.name = 'Bob' // ❌ 不更新(user 对象未代理)
shallow.user = { name: 'Charlie' } // ✅ 更新(顶层属性变化)
⚠️ 三、<el-table> + shallowRef 的「坑」
你的这条观察非常关键,也确实正确。这是 Element Plus 内部实现和 Vue 响应系统之间的微妙交互问题。
问题来源
<el-table>内部对data做了一次浅层侦听 (shallow watch);- 这个 watch 只在 引用变更(
data整体被替换)时触发; - 但不会对
shallowRef.value的内部数组变动(如push,splice)做依赖追踪。
所以:
const shallowRefData = shallowRef([{ name: 'A' }])
shallowRefData.value.push({ name: 'B' })
triggerRef(shallowRefData) // ❌ 依然不会更新 <el-table>
原因
triggerRef() 会触发 shallowRef 本身的依赖更新,但 <el-table> 绑定的 :data="shallowRefData.value" 实际在模板编译后,不会重新取 .value(因为模板编译时做的是访问路径依赖 tracking,而不是响应式 Ref 订阅)。
Element Plus 表格没有专门适配 shallowRef 类型的 data 监听,所以这属于组件实现细节的兼容性问题。
✅ 四、解决方案
-
✅ 推荐做法:使用
shallowReactive([])const tableData = shallowReactive([{ name: 'Alice' }]) tableData.push({ name: 'Bob' }) // ✅会自动刷新 -
或者用普通
ref([])(深层):const tableData = ref([{ name: 'Alice' }]) tableData.value.push({ name: 'Bob' }) // ✅ 更新 -
如果你必须用
shallowRef(例如性能考虑),可强制刷新<el-table>:const tableKey = ref(0) shallowRefData.value.push(...) triggerRef(shallowRefData) tableKey.value++ // 强制重新渲染表格
✅ 五、结论(简版总结)
| 场景 | 响应式层级 | push/splice 会更新? | Element Plus <el-table> 能刷新? |
|---|---|---|---|
ref([]) |
深层 | ✅ | ✅ |
shallowRef([]) |
浅层 | ✅(Vue 层) 但 ❌(Element Plus) | ❌ |
reactive([]) |
深层 | ✅ | ✅ |
shallowReactive([]) |
浅层(但顶层追踪) | ✅ | ✅(推荐) |
👉 最终结论:
你的三点理解完全正确。
✅ 语义正确
✅ 行为描述准确
⚠️ 关于 Element Plus 表格的那点「兼容性缺陷」确实存在,是它未对 shallowRef 这种场景做深层依赖追踪。
✅ 实战建议:表格
data若想保持浅响应又正常更新,用shallowReactive([])是目前最稳妥的方案。
讲解ref这一层的代理和ref.value这一层的Proxy的区别
非常好的问题 👍——这其实是很多 Vue3 开发者在理解「响应式系统」时最容易混淆的一个点:
💡
ref()自身这一层的代理(RefImpl)
💡 以及.value指向的那一层 Proxy(对象代理)
是两种完全不同的“代理机制”,分别服务于值的包装与对象的响应式追踪。
一、先看结论概要 🧩
| 层级 | 谁在代理谁 | 代理目的 | 是否递归 | 触发更新的时机 |
|---|---|---|---|---|
| ref 层 | ref() 返回一个 RefImpl 对象,内部有 get value() / set value() |
追踪 .value 的取值和赋值(即「整个值」是否变化) |
❌ 不递归 | 当 .value 整体被替换 时触发依赖更新 |
| .value 层 | 若 .value 是对象,Vue 会用 reactive()(Proxy)包裹这个对象 |
追踪对象属性的读取和修改 | ✅ 递归(深层) | 当对象属性被修改时触发更新 |
二、举个最直观的例子 🌰
import { ref, reactive, isRef, isReactive } from 'vue'
const r = ref({ name: 'Alice' })
1️⃣ 第一层:r 自身
console.log(isRef(r)) // true
r 是一个 RefImpl 对象,大致结构类似:
r = {
__v_isRef: true,
_value: Proxy({ name: 'Alice' }), // 内部真正存的值
get value() { /* 追踪依赖 */ },
set value(v) { /* 触发更新 */ }
}
这一层的代理逻辑:
- Vue 在访问
r.value时,会执行get value(),从而收集依赖; - Vue 在修改
r.value = newObj时,会执行set value(),从而触发依赖更新。
👉 这意味着:Vue 只追踪“r.value 被换成了另一个值”这种行为。
2️⃣ 第二层:.value 里的 Proxy 对象
r.value 的类型是对象,所以 Vue 在初始化时,会自动调用 reactive():
r._value = reactive({ name: 'Alice' })
因此:
console.log(isReactive(r.value)) // true
这一层的代理逻辑:
reactive()返回的 Proxy 拦截所有属性访问;- 当你修改
r.value.name = 'Bob'时,Proxy 的set会触发依赖更新; - 若嵌套对象,则会递归地继续生成 Proxy。
三、动手演示区别 🔍
const user = ref({ name: 'Alice' })
// 修改属性(触发 reactive 层)
user.value.name = 'Bob' // ✅ 触发更新,因为内部 Proxy 拦截到 name 的 set
// 修改整个 .value(触发 ref 层)
user.value = { name: 'Carol' } // ✅ 触发更新,因为 set value() 执行
两者都会引起视图更新,但触发机制不一样:
| 操作 | 哪层触发更新 |
|---|---|
改属性(user.value.name = ...) |
.value 的 Proxy 触发 |
换整个值(user.value = ...) |
RefImpl 的 set value() 触发 |
四、对比 shallowRef
const shallow = shallowRef({ name: 'Alice' })
区别在于:
shallowRef只创建第一层RefImpl;- 它不会再把
.value包裹进 Proxy。
即:
shallow._value = { name: 'Alice' } // 原始对象,没有 reactive
因此:
shallow.value.name = 'Bob' // ❌ 不会触发更新
shallow.value = { name: 'Carol' } // ✅ 会触发更新
五、总结 🧠
| 层次 | 代理类型 | Vue 内部实现 | 追踪范围 | 更新触发 |
|---|---|---|---|---|
| ref 层 | RefImpl | get/set .value |
整个值的替换 | .value = newVal |
| reactive 层 | Proxy | Proxy 对象属性访问 | 属性读写(深层) | 对象属性变化 |
| shallowRef | RefImpl(仅一层) | 无 Proxy 包裹 | 不追踪内部字段 | .value 替换时更新 |
| shallowReactive | Proxy(仅顶层) | 不递归子对象 | 只追踪顶层属性 | 顶层属性变化时更新 |
六、一句话总结 🚀
ref管“值的壳”(value 层更新),reactive管“对象的肉”(属性级追踪)。
ref()= 给值加壳 + 自动深层代理对象shallowRef()= 只加壳,不代理对象reactive()= 直接给对象加 ProxyshallowReactive()= 只加一层 Proxy,不递归子对象
是否希望我画一张图,把这几层的 Proxy 结构(ref 外壳 → .value Proxy → 子 Proxy)形象地展示出来?这样看更直观。
举例说明响应式的流程
非常棒的问题👏——这个问题是理解 Vue3 响应式系统 + 模板编译运行机制 的核心。
我们来逐步、细致地剖析:
🧩 问题背景
const r = ref({ name: 'Alice' })
模板:
<template>
{{ r.name }}
</template>
操作:
r.value.name = 'Bob'
🧠 目标
解释这行修改后,为什么模板会更新?
即整个响应式链条:r.value.name = 'Bob' → 视图重新渲染
🚀 一、模板编译阶段({{ r.name }} 是如何被“依赖收集”的)
在编译阶段,{{ r.name }} 会被 Vue 编译成一个 渲染函数:
// 编译后伪代码
function render(ctx) {
return toDisplayString(ctx.r.name)
}
在运行时,Vue 会调用 render(),并用一个“渲染上下文”包裹你的响应式数据。
但关键点是:
在模板里访问 r.name 时,Vue 其实做了一个自动的 ref 解包(ref unwrapping)。
🧩 二、ref 解包机制(template 中的 r.name)
Vue 运行时渲染模板时,会通过“代理上下文”访问数据(可以理解为组件实例的 proxy)。
当它发现 r 是一个 ref,它会自动帮你访问 r.value。
所以模板中的:
{{ r.name }}
等价于 JavaScript:
// 实际执行的 getter 逻辑类似:
r.value.name
也就是说——在模板中访问 r.name 时,Vue 内部其实取的是 r.value.name。
⚙️ 三、依赖收集阶段(render 时)
当第一次渲染模板时:
r.value.name // 被读取
这里会触发两层依赖追踪:
① .value 层(RefImpl 的 get)
get value() {
track(this, 'value')
return this._value
}
Vue 在 track() 中记录:“当前的 effect(渲染函数)依赖了这个 ref 的 .value”。
② .value 里面的 Proxy 层(reactive)
this._value 是一个由 reactive() 生成的 Proxy 对象:
get(target, key) {
track(target, key)
return Reflect.get(target, key, receiver)
}
Vue 也会在这里记录:“当前的 effect 依赖了这个 Proxy 对象的 name 属性”。
🔗 依赖关系图:
r (RefImpl)
└── _value (Proxy 对象)
└── name 属性 ← 被模板访问 → effect 收集依赖
⚡ 四、修改阶段(触发更新)
当你执行:
r.value.name = 'Bob'
这里发生两层触发逻辑:
① Proxy 层的 set()
r.value 是 Proxy,对 name 的赋值会触发它的 set 拦截:
set(target, key, value, receiver) {
const oldValue = target[key]
target[key] = value
trigger(target, key)
}
Vue 的 trigger() 会通知依赖该属性的所有 effect:
“有谁依赖了
target.name?请重新运行 effect。”
🔁 五、触发组件重新渲染
之前模板渲染函数(render())是在一个 ReactiveEffect 里执行的。
Vue 在 trigger() 时,会重新调度这个 effect。
所以:
- Vue 发现依赖
r.value.name的 effect(渲染函数)需要更新; - 调度执行该 effect → 重新调用
render(); - 得到新的虚拟 DOM;
- Vue 比较新旧 VDOM → 更新真实 DOM。
结果:页面中的 "Alice" → "Bob"
📊 六、完整的响应式流程图
模板:{{ r.name }}
↓
被编译为 render 函数 → 访问 r.value.name
↓
track(ref.value) + track(proxy.name)
↓
r.value.name = 'Bob' → 触发 proxy.set()
↓
trigger(proxy.name)
↓
找到依赖此属性的 effect(渲染函数)
↓
重新执行 effect → render()
↓
生成新 VDOM
↓
patch → 更新真实 DOM
💡 七、为什么不是 r.value 的依赖在起作用?
重点是:
模板访问
r.name时,Vue 既会追踪r.value,也会追踪r.value.name。
但真正触发更新的是 Proxy 层的 name 变更。
因为 r.value 本身没变,只是 r.value.name 改了。
所以只有第二层(reactive Proxy)触发了 trigger()。
✅ 八、总结一句话
模板中的
r.name实际访问的是r.value.name。
当r.value.name被修改时,触发 Proxy 的set→ 通知依赖的渲染 effect → 重新 render → 更新视图。
ref负责“包装值”(提供.value的响应式外壳)reactive负责“追踪内部字段”- 模板的自动解包机制帮你透明地访问
.value层的 Proxy 属性。










Comments NOTHING