Vue 双向绑定与响应式原理:从源码层面彻底搞懂 目录一、先搞清楚两个概念响应式 vs 双向绑定二、Vue 2 响应式原理Object.defineProperty2.1 核心思路2.2 源码实现defineReactive2.3 数据代理把 data 挂到 this 上2.4 Vue 2 的三大缺陷三、Vue 3 响应式原理Proxy3.1 为什么用 Proxy3.2 核心数据结构targetMap3.3 reactive 源码实现3.4 Handlersget 拦截依赖收集 track3.5 effect副作用函数3.6 ref 的实现四、双向绑定v-model 的本质4.1 v-model 是语法糖4.2 完整闭环4.3 组件上的 v-modelVue 3.4 的 defineModel五、一张图总结六、面试高频追问与回答策略写在最后读完这篇文章你将彻底理解 Vue 的响应式系统是如何“感知”数据变化的以及v-model这个“魔法”背后到底是什么。很多前端开发者每天都在用 Vue知道“数据变了视图会自动更新”但一旦问到“怎么实现的”就开始支支吾吾了。面试官想听的不是背概念而是你能从源码层面讲清楚数据劫持、依赖收集、派发更新这三个环节是如何协同工作的。今天这篇文章带你从源码层面彻底搞懂 Vue 的响应式原理和双向绑定。一、先搞清楚两个概念响应式 vs 双向绑定很多人把“响应式”和“双向绑定”混为一谈其实它们是不同层面的东西概念方向核心机制典型 API响应式Reactivity数据 → 视图数据变化自动更新视图reactive、ref、computed双向绑定Two-way Binding数据 ⇄ 视图数据变视图更新视图变数据也更新v-model响应式是“单向”的数据变了视图跟着变。双向绑定是“响应式 事件监听”数据变视图更新用户在视图上的操作如输入也能反过来修改数据。双向绑定的本质是响应式系统数据→视图 DOM 事件监听视图→数据。搞清楚这个关系下面的源码分析你就能对号入座了。二、Vue 2 响应式原理Object.defineProperty2.1 核心思路Vue 2 的响应式系统核心是Object.defineProperty—— 通过它给对象的每个属性定义getter和setter在属性被访问和修改时进行拦截。Vue 2 的响应式源码主要分布在core/observer目录下涉及三个核心角色角色职责Observer递归遍历数据对象用defineReactive把每个属性转为 getter/setterDepDependency依赖收集器每个响应式属性都有一个 Dep 实例用来存放所有依赖它的 WatcherWatcher观察者负责执行更新操作如渲染组件、执行 computed 回调2.2 源码实现defineReactive下面是 Vue 2 源码中defineReactive的核心逻辑简化版function defineReactive(obj, key, val) { // 每个属性都有一个独立的 Dep 实例 const dep new Dep(); // 递归处理嵌套对象 const childOb observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // 【依赖收集】当属性被读取时将当前 Watcher 添加到 dep 中 if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); // 数组/对象子属性也要收集 } } return val; }, set: function reactiveSetter(newVal) { if (newVal val) return; val newVal; // 【派发更新】当属性被修改时通知所有依赖它的 Watcher dep.notify(); } }); }2.3 数据代理把data挂到this上我们在组件里写this.name就能访问到data中的name这也是通过Object.defineProperty做了一层代理// Vue 源码中的 proxy 函数简化版 function proxy(target, sourceKey, key) { Object.defineProperty(target, key, { get: function() { return this[sourceKey][key]; // 实际返回 this._data.name }, set: function(val) { this[sourceKey][key] val; // 实际设置 this._data.name val } }); }当我们写this.name时实际上访问的是this._data.name而_data里的属性已经被defineReactive劫持过了。2.4 Vue 2 的三大缺陷这也是为什么 Vue 3 要重构响应式系统的根本原因无法监听对象属性的新增和删除Object.defineProperty只能劫持已存在的属性新增属性需要用Vue.set数组变更无法监听通过下标修改数组arr[0] xxx无法触发更新所以要 hack 数组的push、pop等变异方法深层监听性能问题初始化时要递归遍历整个对象嵌套越深性能越差三、Vue 3 响应式原理Proxy3.1 为什么用 ProxyVue 3 用Proxy全面重构了响应式系统源码在packages/reactivity目录下。Proxy 直接代理整个对象而不是对象的某个属性所以✅ 支持动态新增/删除属性✅ 支持数组下标修改✅ 惰性监听只有访问到深层属性时才递归代理✅ 原生支持 Map、Set 等集合类型3.2 核心数据结构targetMapVue 3 的依赖管理采用WeakMapMapSet三层结构// targetMap: 存储所有响应式对象的依赖关系 // WeakMaptarget, Mapkey, Seteffect const targetMap new WeakMap(); // 结构示意 // targetMap { // userObj { // name [effect1, effect2], // age [effect1] // } // }WeakMap键是响应式对象弱引用不影响垃圾回收Map键是对象的属性名Set存储依赖这个属性的所有 effect副作用函数3.3 reactive 源码实现reactive的入口在packages/reactivity/src/reactive.ts// reactive 入口 export function reactive(target: object) { if (isReadonly(target)) { return target; } return createReactiveObject( target, false, mutableHandlers, // 普通对象的 handlers mutableCollectionHandlers, // 集合类型的 handlers reactiveMap ); } // 创建响应式代理对象 function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) { // 1. 如果不是对象直接返回 if (!isObject(target)) return target; // 2. 如果已经是代理对象直接返回 if (target[ReactiveFlags.RAW]) return target; // 3. 如果已经代理过了从缓存中取 const existingProxy proxyMap.get(target); if (existingProxy) return existingProxy; // 4. 创建 Proxy 代理 const proxy new Proxy( target, targetType TargetType.COLLECTION ? collectionHandlers : baseHandlers ); // 5. 缓存代理结果 proxyMap.set(target, proxy); return proxy; }3.4 Handlersget 拦截依赖收集 track当访问响应式对象的属性时会触发get拦截器执行依赖收集trackconst mutableHandlers { get(target, key, receiver) { // 如果是特殊标记直接返回 if (key ReactiveFlags.IS_REACTIVE) return true; // 【依赖收集】将当前正在执行的 effect 与这个属性关联起来 track(target, key); // 获取值 const res Reflect.get(target, key, receiver); // 【惰性代理】如果取到的值还是对象递归代理只有在访问时才代理不是初始化时 if (isObject(res)) { return reactive(res); } return res; }, set(target, key, value, receiver) { const oldValue target[key]; const result Reflect.set(target, key, value, receiver); // 【派发更新】只有值真正变化时才触发更新 if (oldValue ! value) { trigger(target, key, value, oldValue); } return result; } };3.5 effect副作用函数effect是 Vue 3 响应式系统的“发动机”// effect 函数创建一个响应式副作用 function effect(fn) { const _effect new ReactiveEffect(fn); _effect.run(); // 立即执行触发依赖收集 return _effect; } class ReactiveEffect { active true; deps []; // 记录这个 effect 依赖了哪些属性 constructor(fn) { this.fn fn; } run() { if (!this.active) return this.fn(); // 将当前 effect 设置为全局激活状态 const lastEffect activeEffect; activeEffect this; // 执行 fn → 内部访问响应式数据 → 触发 get → 触发 track 收集依赖 const result this.fn(); activeEffect lastEffect; // 恢复 return result; } }整个流程effect(fn)执行 →fn内部读取响应式数据 → 触发get拦截 →track把当前effect存进targetMap→ 数据修改时触发set→trigger从targetMap取出所有effect重新执行。3.6 ref 的实现ref是用来包装基本类型的它的核心是创建一个带有.value属性的对象同样通过 getter/setter 实现依赖收集和派发更新function ref(value) { return createRef(value, false); } function createRef(rawValue, shallow) { return new RefImpl(rawValue, shallow); } class RefImpl { _value; _rawValue; dep; // 每个 ref 有自己的 Dep constructor(value) { this._rawValue value; this._value convert(value); // 如果是对象用 reactive 包装 this.dep new Dep(); } get value() { // 依赖收集 track(this, value); return this._value; } set value(newVal) { if (newVal ! this._rawValue) { this._rawValue newVal; this._value convert(newVal); // 派发更新 trigger(this, value); } } }四、双向绑定v-model 的本质搞懂了响应式系统数据 → 视图我们来看双向绑定的另一半视图 → 数据。4.1 v-model 是语法糖v-model本质上就是:valueinput的语法糖!-- 你写的 -- input v-modeluser.name / !-- 编译后实际上变成 -- input :valueuser.name inputuser.name $event.target.value /$event.target.value是原生 DOM 事件对象中的输入值。4.2 完整闭环双向绑定的完整流程是数据 → 视图响应式系统的setter触发 →dep.notify()→Watcher更新 → 视图重新渲染input的value被更新视图 → 数据用户在输入框中打字 → 触发input事件 → 执行user.name $event.target.value→ 触发响应式数据的setter→ 回到步骤 1这就形成了一个闭环用户输入改变数据数据改变又驱动视图更新。4.3 组件上的 v-modelVue 3.4 的 defineModel在自定义组件上使用v-model时Vue 3.4 引入了defineModel宏让双向绑定更简洁!-- 父组件 -- Child v-modelcount / !-- 子组件Vue 3.4 -- script setup // defineModel 返回一个 ref与父组件的 v-model 双向同步 const model defineModel(); // 修改 model.value 会自动同步到父组件 model.value; /scriptdefineModel底层依然是propsemits的模式只是把模板代码封装成了宏。五、一张图总结六、面试高频追问与回答策略Q1Vue 2 和 Vue 3 的响应式有什么区别Vue 2 用Object.defineProperty劫持对象属性缺点是无法监听新增/删除属性和数组下标修改且初始化时要递归遍历整个对象。Vue 3 用Proxy代理整个对象支持动态属性、数组变更、集合类型且采用惰性代理——只有访问到深层属性时才递归性能更好。Q2v-model和v-bind有什么区别v-bind是单向绑定数据→视图v-model是双向绑定数据⇄视图。v-model本质上是v-bind:valueinput事件监听的语法糖。Q3reactive和ref有什么区别什么时候用哪个reactive只能代理对象返回的是 Proxy 代理对象ref可以包装任意类型包括基本类型返回的是带有.value属性的 RefImpl 实例。对象用reactive基本类型用ref如果解构reactive对象会丢失响应性需要用toRefs转换。写在最后Vue 的响应式系统本质上就是“数据劫持 发布-订阅模式”数据劫持Vue 2 用Object.definePropertyVue 3 用Proxy拦截数据的读和写依赖收集在数据被读取时getter把当前正在执行的 Watcher/Effect 记录下来派发更新在数据被修改时setter通知所有依赖它的 Watcher/Effect 重新执行双向绑定则是在这个基础上加上了 DOM 事件监听input让视图的变化能反向修改数据。理解了这套机制你就能明白为什么Vue.set存在、为什么数组有些操作不触发更新、为什么ref要用.value访问——所有 API 设计背后都有源码层面的必然逻辑。PS本文由deepseek整理生成