1. 从 Vue2 到 Vue3为什么我们需要 Proxy如果你用过 Vue2肯定对它的响应式系统又爱又恨。爱的是你只需要在data里定义好数据Vue 就能神奇地帮你更新视图开发效率直线上升。恨的是你可能会遇到一些“灵异事件”比如给对象新增一个属性视图没反应或者通过数组下标直接修改元素页面也不更新。为了解决这些问题我们不得不求助于Vue.set或this.$set这类 API写起来总感觉有点别扭。这些问题的根源就在于 Vue2 响应式系统的基石Object.defineProperty。这个 API 有个天生的限制——它只能拦截对象已有属性的读取和设置操作。对于对象上不存在的属性或者数组的length变化它就无能为力了。Vue2 的解决方案是在初始化时递归遍历整个对象把能劫持的属性都劫持一遍。但这也带来了性能开销尤其是对于深层嵌套的大对象。Vue3 的响应式系统则换上了全新的引擎Proxy。你可以把 Proxy 理解为一个“超级门卫”或者“全能拦截器”。它不像Object.defineProperty那样只能盯着对象的几个固定属性而是可以监听整个对象的所有操作包括属性的增删改查、数组的push、pop甚至是用in操作符检查属性是否存在。这个“门卫”能力强大而且不需要在一开始就把对象里里外外翻个底朝天它是“按需拦截”的只有在属性被访问时才会建立联系性能上自然更有优势。我刚开始接触 Proxy 时觉得它概念上有点绕。后来我把它想象成一个“智能代理秘书”。你有一个原始对象老板但你不直接跟老板打交道所有事情都通过秘书Proxy来处理。秘书会记录下谁依赖来找老板要过什么资料get也会在老板的资料有变动时set主动通知所有相关的人。这样一来管理起来就清晰多了。所以Vue3 转向 Proxy 不是简单的技术升级而是为了解决 Vue2 响应式系统的根本性短板为更复杂、更动态的应用场景铺平道路。接下来我们就亲手揭开这位“全能门卫”的神秘面纱。2. 亲手打造一个简易的 Proxy 响应式系统光说不练假把式要真正理解 Proxy 如何驱动响应式最好的办法就是自己动手实现一个迷你版本。别担心我们一步步来保证你能看懂。2.1 核心三剑客Reactive、Effect 和 TargetMapVue3 的响应式核心围绕着三个关键概念构建我们先来认识一下它们Reactive这是我们的明星函数它的任务就是用一个 Proxy 把普通对象“包裹”起来使其变成响应式对象。我们调用const state reactive({ count: 0 })得到的state就是一个被 Proxy 代理的对象。Effect翻译过来是“副作用”。在 Vue 的语境里它可以理解为一段依赖响应式数据的代码。最常见的就是我们的渲染函数或者computed、watch。当它所依赖的响应式数据变化时这个effect需要被重新执行。我们可以创建一个effect函数来包裹我们的逻辑effect(() { console.log(state.count) })。TargetMap这是一个全局的“依赖关系地图”。它的结构是一个WeakMap键Key是原始对象Target值Value是另一个Map。这个子Map的键是原始对象的属性名Key值是一个Set里面存储了所有依赖这个属性的effect函数。这个结构是依赖收集和触发的核心数据库。听起来有点复杂我们画个简单的脑图来理解它们的关系TargetMap (WeakMap) │ ├── 原始对象 targetA: DepsMap (Map) │ │ │ ├── 属性 keyA: [ effect1, effect2 ] (Set) │ └── 属性 keyB: [ effect3 ] (Set) │ └── 原始对象 targetB: DepsMap (Map) │ └── 属性 keyC: [ effect4 ] (Set)当state.count被读取时我们需要把当前正在执行的effect记录到targetA即state的原始对象的count属性对应的Set里。当state.count被修改时我们就去这个Set里找到所有effect并执行它们。2.2 实现第一步基础的 reactive 与 effect让我们用代码来实现这个最基础的模型。首先我们定义那两个核心的全局存储。// 全局依赖存储 const targetMap new WeakMap(); // key: 原始对象, value: depsMap let activeEffect null; // 当前正在执行的 effect // effect 函数用来注册副作用 function effect(fn) { activeEffect fn; // 执行前标记当前活跃的 effect fn(); // 执行函数触发内部的 getter从而收集依赖 activeEffect null; // 执行完毕清空标记 }接下来是重头戏reactive函数。我们创建一个 Proxy在它的get和set拦截器里实现依赖的追踪和触发。function reactive(target) { const handler { get(target, key, receiver) { track(target, key); // 依赖收集 return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const oldValue target[key]; const result Reflect.set(target, key, value, receiver); if (oldValue ! value) { // 只有值真正变化了才触发更新 trigger(target, key); // 依赖触发 } return result; } }; return new Proxy(target, handler); } // 依赖收集把 activeEffect 存起来 function track(target, key) { if (!activeEffect) return; // 没有活跃的 effect直接返回 let depsMap targetMap.get(target); if (!depsMap) { depsMap new Map(); targetMap.set(target, depsMap); } let dep depsMap.get(key); if (!dep) { dep new Set(); depsMap.set(key, dep); } dep.add(activeEffect); // 将当前 effect 加入依赖集合 } // 依赖触发执行所有收集到的 effect function trigger(target, key) { const depsMap targetMap.get(target); if (!depsMap) return; const dep depsMap.get(key); if (dep) { dep.forEach(effect effect()); // 重新执行每个 effect } }现在让我们测试一下这个迷你响应式系统const state reactive({ count: 0 }); // 创建一个 effect依赖 state.count effect(() { console.log(count 发生了变化新值是${state.count}); }); // 首次执行输出: count 发生了变化新值是0 state.count 1; // 自动触发 effect输出: count 发生了变化新值是1 state.count 2; // 再次触发输出: count 发生了变化新值是2看一个最核心的响应式模型就完成了当我们修改state.count时之前注册的effect函数自动执行了。这就是 Vue3 响应式系统最本质的原理在 get 时收集依赖在 set 时触发更新。2.3 解决边缘情况嵌套对象、数组与“浅”响应我们的基础版本已经能跑了但离实用还有距离。第一个问题嵌套对象。如果我们的数据是{ user: { name: 小明 } }修改state.user.name能触发更新吗目前不行因为我们的 Proxy 只代理了最外层。我们需要在get拦截器里做点手脚如果取到的值是一个对象我们就递归地把它也用reactive包装一下再返回。这就是 Vue3 中reactive是“深响应”的原因。第二个问题数组。Proxy 可以完美拦截数组的方法比如push、pop、splice。这些方法会修改数组自身length属性或索引我们的set拦截器能处理吗可以的因为array.push(1)本质上会设置array[array.length] 1并修改length这两步都会被set拦截器捕获。所以我们的基础版本其实已经能处理数组变更了这是相比 Vue2 的一个巨大优势。第三个概念shallowReactive。有时候我们不需要深响应比如一个巨大的对象列表我们只关心列表本身的引用变化不关心内部每个对象的变化。这时就可以用shallowReactive。它的实现非常简单就是在get拦截器里不进行递归包装直接返回原始值。Vue3 提供了这个 API 给需要精细控制性能的场景。通过一步步完善这些细节我们就能越来越深刻地体会到 Proxy 带来的设计上的简洁与强大。在 Vue2 里需要绞尽脑汁处理的数组和新增属性问题在 Proxy 的世界里几乎都是免费获得的特性。3. 与 Composition API 的完美协同Ref 与 Reactive 的抉择有了自己实现reactive的经验我们再来看 Vue3 的 Composition API 就豁然开朗了。Composition API 的核心是让我们能更灵活地组织代码逻辑而响应式系统是它的基石。这里有两个最重要的响应式 APIref和reactive。很多新手会困惑我该用哪个3.1 Ref为原始值穿上响应式外衣reactive有一个限制它只接受对象。那像数字、字符串这样的原始值怎么办这就是ref的用武之地。ref通过一个简单的“装箱”操作来解决这个问题它创建一个带有.value属性的响应式对象这个对象的.value属性指向内部值。我们可以基于之前的reactive快速实现一个reffunction ref(value) { return reactive({ value }); }当然Vue3 内部的ref实现更高效它用的是类似我们之前实现的track和trigger但概念上你可以这么理解。使用起来是这样的import { ref } from vue; const count ref(0); // 创建一个响应式引用 console.log(count.value); // 读取值0 count.value 1; // 修改值触发更新你可能觉得每次都要写.value很麻烦。但在模板和reactive对象中Vue 会自动帮你“解包”。例如在模板中你可以直接写{{ count }}在reactive对象里赋值给一个属性访问时也会自动解包。什么时候用ref当你定义的是一个原始值字符串、数字、布尔值时。当你定义的是一个可能被替换的引用比如一个对象但后续可能用另一个全新对象赋值时。因为reactive代理的是原对象如果你给reactive的变量重新赋值一个新对象会失去响应性。而ref的.value赋值总是有效的。在逻辑提取到自定义组合式函数Composable中时返回ref是更通用的做法。3.2 Reactive对象的最佳拍档对于对象和数组reactive是更自然的选择。它让你可以直接访问和修改属性无需.value。import { reactive } from vue; const state reactive({ user: { name: 小明 }, hobbies: [篮球, 音乐] }); state.user.name 小红; // 深层次修改响应式生效 state.hobbies.push(阅读); // 数组操作响应式生效什么时候用reactive当你定义的是一个不需要整体替换的复杂对象时。当你需要深响应式并且享受直接访问属性的便利时。在组件本地状态管理时结构清晰。在实际项目中我的经验是组件的核心状态一个对象用reactive而零散的、独立的原始值或可能被替换的引用用ref。两者经常混合使用比如在一个reactive对象里包含几个ref这是完全没问题的。3.3 组合式函数逻辑复用的新篇章理解了ref和reactiveComposition API 的精髓——组合式函数Composable就水到渠成了。组合式函数就是利用 Vue 的响应式 API 来封装和复用有状态逻辑的函数。举个例子我们需要一个跟踪鼠标位置的逻辑。在 Vue2 的 Options API 里我们得把data、mounted、beforeDestroy拆开到不同选项里。现在我们可以这样写// useMouse.js import { ref, onMounted, onUnmounted } from vue; export function useMouse() { const x ref(0); const y ref(0); function update(event) { x.value event.pageX; y.value event.pageY; } onMounted(() window.addEventListener(mousemove, update)); onUnmounted(() window.removeEventListener(mousemove, update)); return { x, y }; // 返回 ref让使用者可以解构 }然后在组件里使用script setup import { useMouse } from ./useMouse; const { x, y } useMouse(); /script template p鼠标位置{{ x }}, {{ y }}/p /template看逻辑被完美地封装和复用了这正是 Proxy 驱动的响应式系统与 Composition API 协同带来的威力响应式数据可以独立于组件实例被创建和管理使得逻辑关注点分离变得极其自然和强大。4. MVVM 实战用响应式系统构建动态表单组件理论讲得再多不如来一个实战。我们利用刚学到的 Proxy 响应式知识和 Composition API构建一个常见的动态表单组件。这个表单允许用户动态添加/删除字段并且所有字段的值都是响应式的。4.1 设计组件状态与结构首先我们设计组件的状态。一个表单字段至少需要label标签名、type输入类型、value值。整个表单就是这些字段的数组。script setup import { reactive } from vue; // 使用 reactive 定义表单状态它是一个字段数组 const formState reactive({ fields: [ { id: 1, label: 姓名, type: text, value: }, { id: 2, label: 邮箱, type: email, value: }, { id: 3, label: 年龄, type: number, value: }, ] }); /script我们用reactive包裹整个状态对象这样无论是修改fields数组本身增删还是修改数组里某个对象的value都能触发视图更新。4.2 实现动态增删与表单绑定接下来我们在模板中渲染这个动态表单并实现添加和删除功能。template div classdynamic-form div v-forfield in formState.fields :keyfield.id classform-field label :forfield-${field.id}{{ field.label }}/label !-- 使用 v-model 绑定到 field.value这是响应式的核心 -- input :idfield-${field.id} :typefield.type v-modelfield.value / button clickremoveField(field.id) typebutton删除/button /div div classactions button clickaddField typebutton 添加字段/button button clicksubmitForm typebutton提交/button /div div classpreview h4实时预览JSON/h4 pre{{ formDataPreview }}/pre /div /div /template注意看input v-modelfield.value这一行。v-model在这里完美地工作正是因为field是reactive对象中的一个响应式属性。当用户在输入框打字时field.value被修改触发我们之前实现的set拦截进而驱动视图更新。然后我们实现添加和删除的方法script setup import { reactive, computed } from vue; const formState reactive({ fields: [ /* ... 初始字段 ... */ ], nextId: 4 // 用于生成新字段的ID }); function addField() { formState.fields.push({ id: formState.nextId, label: 新字段 ${formState.nextId}, type: text, value: }); } function removeField(id) { const index formState.fields.findIndex(f f.id id); if (index -1) { formState.fields.splice(index, 1); } } // 使用 computed 生成表单数据的预览 const formDataPreview computed(() { return formState.fields.reduce((acc, field) { acc[field.label] field.value; return acc; }, {}); }); function submitForm() { // 在实际项目中这里会发送数据到后端 console.log(提交的数据, formDataPreview.value); alert(表单数据已准备提交请查看控制台。); } /script这里有几个关键点addField中使用push我们直接操作formState.fields数组。得益于 Proxy 对数组方法的拦截这个push操作会触发响应式更新视图会自动渲染出新字段。removeField中使用splice同样splice也能被 Proxy 拦截确保删除操作是响应式的。使用computedformDataPreview是一个计算属性它依赖formState.fields。当任何字段的value变化或者字段增删时这个计算属性会自动重新计算。这就是响应式依赖链的威力。4.3 样式与功能增强为了让组件更好用我们可以加一点样式和额外功能比如字段类型选择。style scoped .dynamic-form { max-width: 500px; margin: 2rem auto; padding: 1rem; border: 1px solid #eee; border-radius: 8px; } .form-field { display: flex; align-items: center; margin-bottom: 1rem; gap: 0.5rem; } .form-field label { min-width: 60px; } .form-field input, .form-field select { flex-grow: 1; padding: 0.5rem; } .actions { margin-top: 1rem; display: flex; gap: 0.5rem; } .preview { margin-top: 2rem; padding: 1rem; background-color: #f5f5f5; border-radius: 4px; } /style在模板中我们可以为每个字段增加一个类型下拉框template div v-forfield in formState.fields :keyfield.id classform-field label :forlabel-${field.id}标签/label input :idlabel-${field.id} v-modelfield.label / label :fortype-${field.id}类型/label select :idtype-${field.id} v-modelfield.type option valuetext文本/option option valueemail邮箱/option option valuenumber数字/option option valuepassword密码/option /select input :typefield.type :placeholder请输入${field.label} v-modelfield.value / button clickremoveField(field.id) typebutton删除/button /div /template现在这个动态表单组件已经具备了完整的 CRUD 功能并且所有状态都是响应式的。你可以添加字段、删除字段、修改字段的标签、类型和值底部的 JSON 预览会实时变化。当你点击提交时就能拿到结构化的表单数据。通过这个实战项目你可以清晰地看到基于 Proxy 的响应式系统让开发变得多么直观。我们几乎不需要考虑“如何通知视图更新”只需要专注于修改数据本身剩下的都交给 Vue。这种开发体验正是 MVVM 模式所追求的数据驱动视图开发者关注业务逻辑而非 DOM 操作。从自己实现简易响应式系统到理解ref和reactive的差异再到用它们构建出实用的动态表单这个过程中 Proxy 作为幕后英雄始终提供着强大而稳定的支持。