框架与状态管理 · KNOWLEDGE CHAPTER
K19

Vue 响应式与数据契约

Vue 响应式可理解为“受追踪的读取建立依赖,受管理的写入通知依赖”。是否响应不由变量名称决定,而由读取位置、代理或 ref 身份、编译转换及组件数据所有权共同决定。

框架与状态管理2 道章节练习6 道主练关联

核心模型

Vue 响应式可理解为“受追踪的读取建立依赖,受管理的写入通知依赖”。是否响应不由变量名称决定,而由读取位置、代理或 ref 身份、编译转换及组件数据所有权共同决定。

原理与机制

保留的是访问关系,不是变量外观

reactive 通过代理属性访问追踪对象;把原始值解构给局部变量,得到的是当时的值,后续已不经过原属性。toRef、toRefs 能保留属性关联,ref 则通过 .value 建立稳定容器。如果解构的是对象,内部仍可能是代理,但父属性换成新对象不会自动改绑旧局部引用。Vue 3.5 的同一 script setup 内 defineProps 解构由编译器转换,属于特定例外;跨函数传一个字符串依然只是当时的实参。

派生值与副作用有不同依赖契约

computed 适合纯派生关系,依赖不变时复用结果;getter 内请求或修改源状态会让求值和副作用纠缠。watch 显式声明来源,watchEffect 追踪同步执行阶段读取,第一次 await 后才读取的值不会因此加入依赖。watch(() => state.user) 默认关注返回身份,直接 watch(state.user) 关注当时那个响应式对象的深层变化,父级替换不让它自动改绑。深度监听也不是生成前后快照,原地修改时新旧参数可能是同一个对象。

更新粒度与计算结果身份需要一致

shallowRef 只为自身 .value 提供浅层追踪,内部原样保存;原始对象嵌套修改不会自动通知,替换根快照或明确 triggerRef 才符合约定。若传入原本的 reactive 代理,它不会把代理变回原始值。computed 缓存不降低首次或失效后的算法复杂度,Vue 3.4+ 的结果稳定性可避免仅依赖未变结果的下游 effect 重跑,但每次返回新对象仍改变身份。复用旧对象前需完成依赖读取与等价判断,不能为优化提前漏掉依赖。

组件输入是所有权协议

组件 v-model 表达 modelValue 输入与 update:modelValue 通知,父组件仍拥有权威值;Vue 3.4+ defineModel 简化这个声明,不让子组件成为父状态主人。对象 prop 的嵌套内容并非自动深度只读,偷改会绕过可审查的数据流。需确认后保存的表单应有局部草稿与提交事件,并定义父级新值到达时的冲突处理。组合函数接收值、ref 或 getter 时,应在 watch 来源或 effect 同步阶段执行 toValue,调用时只解包一次会冻结动态输入。

用对照实验确认追踪范围

排查“没更新”先区分数据确实没变、数据变了但没通知、通知后 DOM 尚未提交三种情况。对同一对象依次测试改内部字段、替换父属性和把值传过函数边界,再观察 getter、watch 回调与组件更新,不能只看模板最终文字。对于 computed 布尔结果,把上游从一个奇数改为另一个奇数,分别计数求值与下游执行;再改为每次返回新对象,能直观看到业务等价和引用等价的差别。

最小示例

javascript
javascript
import { reactive, toRef, computed, shallowRef } from 'vue';
const state = reactive({ count: 1 });
const { count: snapshot } = state;
const count = toRef(state, 'count');
const doubled = computed(() => count.value * 2);
state.count = 3;
console.log(snapshot, count.value, doubled.value); // 1, 3, 6
const rows = shallowRef([{ id: 1, done: false }]);
rows.value = rows.value.map(r => ({ ...r, done: true }));

解构变量停留在 1,关联 ref 读取当前属性 3,computed 得到 6。大列表示例通过新根数组表达变化,使浅层契约清楚;若改成 rows.value[0].done=true,内部原始对象虽变化,却不会因该写入自动通知依赖。示例需 Vue 3 环境,后者不是“对象没改”,而是“没经过被追踪的通知边界”。

环境:安装 Vue 3 的 ESM 项目;展示响应式值与不可变根替换,没有创建 DOM 或完整组件。

核验:已静态审查示例与本章机制一致;未运行此示例,未进行框架或浏览器集成测试。观察结果为待复现的教学预期。

边界与取舍

边界

  • “解构一定失去响应式”过于绝对:对象内部可能仍是代理,Vue 3.5 props 编译转换也有特定范围。
  • computed 缓存不等于永久计算一次;结果为新对象时,业务等价不代表引用稳定。
  • defineModel 的子级默认值与父级 undefined 可能不同步,多模型仍需各自独立输入和更新事件。

取舍

  • 深层追踪便于可变业务数据,浅层不可变快照减少追踪成本,却要求统一替换或通知协议。
  • watch 显式来源更易审查,watchEffect 简洁但依赖隐含;复杂异步与多人维护场景宜优先清楚契约。

口述示范

我先确认数据读取是否经过代理或 ref,再确认这个读取处于哪个追踪上下文。派生值用纯 computed,副作用用明确 watcher,父子数据按 props 与事件契约流动。最后讨论浅层结构及结果身份优化,并注明 Vue 3.4、3.5 的版本边界。

章节练习

练习 1

Vue 3.5 页面对解构 userId 能更新,但 useFetch(userId) 不跟随。如何设计输入?

展开检查点
  • 用 getter 或 ref 传递动态读取,而非已求值字符串。
  • 在受追踪来源内调用 toValue。
  • 测试原始值、嵌套变化、整项替换及 A→B,避免仅依赖类型通过。
练习 2

大型编辑器接入 shallowRef 与双模型表单后出现漏刷新和父级数据被偷改,如何修复?

展开检查点
  • 外部订阅产生新根快照或采用明确 triggerRef 协议。
  • 模型分别声明更新事件,草稿提交不直接改 prop 内部。
  • 区分 getter 求值与下游执行次数,保持 computed 纯净。

参考来源与核验边界

主体为 Vue 3;defineModel、computed 稳定性注明 3.4+,响应式 props 解构注明 3.5+。编译器与运行时应配套,普通 reactive 解构不享有该编译例外。 toValue 为 3.3+;本章对 watchEffect 的说明只追踪同步阶段。

内容版本:2026-10-04.2 · 题目和来源 ID 保持原样,本站不将面经标签解释为企业官方出题或高频保证。