理解笔记

首先需要理解watch和deep的重要概念,详见【Vue巨坑】当 watch 和 deep: true 相遇:一场由“newVal === oldVal”引发的血案

ref和reactive不一样

  • 不设置deep的情况下(默认deep: false,但reactive不太一样是特例),在watch的第一个参数中直接放一个ref,只能监听到第一层的变化(如ref.value的变化),修改ref.value.aref.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.aobj.value.a.b的变化;放obj.value则可以监听到obj.value.aobj.value.a.b的变化。在<template></template中直接写obj.a.b却可以响应式更新。(本质是对于ref而言,只能监听到ref.value的值的变化;而reactive默认深层监听,可以监听到reactive.a.b

举例

如下demo片段,obj.a.bobj.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
  • reactiveshallowReactive时会直接给值包一层Proxy,区别是reactive还会递归对象的属性,给所有的对象属性也包一层Proxy`
  • 【坑】用<el-table>时如果给表格传递的data是shallowRef(如shallowRefData),那么在pushsplice这个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 监听,所以这属于组件实现细节的兼容性问题


✅ 四、解决方案

  1. 推荐做法:使用 shallowReactive([])

    const tableData = shallowReactive([{ name: 'Alice' }])
    tableData.push({ name: 'Bob' }) // ✅  会自动刷新
  2. 或者用普通 ref([])(深层):

    const tableData = ref([{ name: 'Alice' }])
    tableData.value.push({ name: 'Bob' }) // ✅ 更新
  3. 如果你必须用 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 = ... RefImplset 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() = 直接给对象加 Proxy
  • shallowReactive() = 只加一层 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 属性。

A Student on the way to full stack of Web3.