news 2026/6/25 12:00:56

Vue 双向绑定与响应式原理:从源码层面彻底搞懂

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue 双向绑定与响应式原理:从源码层面彻底搞懂

目录

一、先搞清楚两个概念:响应式 vs 双向绑定

二、Vue 2 响应式原理:Object.defineProperty

2.1 核心思路

2.2 源码实现:defineReactive

2.3 数据代理:把 data 挂到 this 上

2.4 Vue 2 的三大缺陷

三、Vue 3 响应式原理:Proxy

3.1 为什么用 Proxy?

3.2 核心数据结构:targetMap

3.3 reactive 源码实现

3.4 Handlers:get 拦截(依赖收集 track)

3.5 effect:副作用函数

3.6 ref 的实现

四、双向绑定:v-model 的本质

4.1 v-model 是语法糖

4.2 完整闭环

4.3 组件上的 v-model:Vue 3.4 的 defineModel

五、一张图总结

六、面试高频追问与回答策略

写在最后


读完这篇文章,你将彻底理解 Vue 的响应式系统是如何“感知”数据变化的,以及v-model这个“魔法”背后到底是什么。

很多前端开发者每天都在用 Vue,知道“数据变了视图会自动更新”,但一旦问到“怎么实现的”,就开始支支吾吾了。面试官想听的,不是背概念,而是你能从源码层面讲清楚:数据劫持、依赖收集、派发更新这三个环节是如何协同工作的。

今天这篇文章,带你从源码层面彻底搞懂 Vue 的响应式原理和双向绑定。

一、先搞清楚两个概念:响应式 vs 双向绑定

很多人把“响应式”和“双向绑定”混为一谈,其实它们是不同层面的东西

概念方向核心机制典型 API
响应式(Reactivity)数据 → 视图数据变化自动更新视图reactiverefcomputed
双向绑定(Two-way Binding)数据 ⇄ 视图数据变视图更新,视图变数据也更新v-model

响应式是“单向”的:数据变了,视图跟着变。双向绑定是“响应式 + 事件监听”:数据变视图更新,用户在视图上的操作(如输入)也能反过来修改数据。

双向绑定的本质是:响应式系统(数据→视图)+ DOM 事件监听(视图→数据)。搞清楚这个关系,下面的源码分析你就能对号入座了。

二、Vue 2 响应式原理:Object.defineProperty

2.1 核心思路

Vue 2 的响应式系统核心是Object.defineProperty—— 通过它给对象的每个属性定义gettersetter,在属性被访问和修改时进行拦截。

Vue 2 的响应式源码主要分布在core/observer目录下,涉及三个核心角色:

角色职责
Observer递归遍历数据对象,用defineReactive把每个属性转为 getter/setter
Dep(Dependency)依赖收集器,每个响应式属性都有一个 Dep 实例,用来存放所有依赖它的 Watcher
Watcher观察者,负责执行更新操作(如渲染组件、执行 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 要重构响应式系统的根本原因:

  1. 无法监听对象属性的新增和删除Object.defineProperty只能劫持已存在的属性,新增属性需要用Vue.set

  2. 数组变更无法监听:通过下标修改数组(arr[0] = xxx)无法触发更新,所以要 hack 数组的pushpop等变异方法

  3. 深层监听性能问题:初始化时要递归遍历整个对象,嵌套越深性能越差

三、Vue 3 响应式原理:Proxy

3.1 为什么用 Proxy?

Vue 3 用Proxy全面重构了响应式系统,源码在packages/reactivity目录下。Proxy 直接代理整个对象,而不是对象的某个属性,所以:

  • ✅ 支持动态新增/删除属性

  • ✅ 支持数组下标修改

  • ✅ 惰性监听(只有访问到深层属性时才递归代理)

  • ✅ 原生支持 Map、Set 等集合类型

3.2 核心数据结构:targetMap

Vue 3 的依赖管理采用WeakMap+Map+Set三层结构:

// targetMap: 存储所有响应式对象的依赖关系 // WeakMap<target, Map<key, Set<effect>>> 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 Handlers:get 拦截(依赖收集 track)

当访问响应式对象的属性时,会触发get拦截器,执行依赖收集(track)

const 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→ 数据修改时触发settriggertargetMap取出所有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本质上就是:value+@input的语法糖

<!-- 你写的 --> <input v-model="user.name" /> <!-- 编译后实际上变成 --> <input :value="user.name" @input="user.name = $event.target.value" />

$event.target.value是原生 DOM 事件对象中的输入值。

4.2 完整闭环

双向绑定的完整流程是:

  1. 数据 → 视图:响应式系统的setter触发 →dep.notify()Watcher更新 → 视图重新渲染,inputvalue被更新

  2. 视图 → 数据:用户在输入框中打字 → 触发input事件 → 执行user.name = $event.target.value→ 触发响应式数据的setter→ 回到步骤 1

这就形成了一个闭环:用户输入改变数据,数据改变又驱动视图更新。

4.3 组件上的 v-model:Vue 3.4 的 defineModel

在自定义组件上使用v-model时,Vue 3.4 引入了defineModel宏,让双向绑定更简洁:

<!-- 父组件 --> <Child v-model="count" /> <!-- 子组件(Vue 3.4+) --> <script setup> // defineModel 返回一个 ref,与父组件的 v-model 双向同步 const model = defineModel(); // 修改 model.value 会自动同步到父组件 model.value++; </script>

defineModel底层依然是props+emits的模式,只是把模板代码封装成了宏。

五、一张图总结

六、面试高频追问与回答策略

Q1:Vue 2 和 Vue 3 的响应式有什么区别?

Vue 2 用Object.defineProperty劫持对象属性,缺点是无法监听新增/删除属性和数组下标修改,且初始化时要递归遍历整个对象。Vue 3 用Proxy代理整个对象,支持动态属性、数组变更、集合类型,且采用惰性代理——只有访问到深层属性时才递归,性能更好。

Q2:v-modelv-bind有什么区别?

v-bind单向绑定(数据→视图),v-model双向绑定(数据⇄视图)。v-model本质上是v-bind:value+@input事件监听的语法糖。

Q3:reactiveref有什么区别?什么时候用哪个?

reactive只能代理对象,返回的是 Proxy 代理对象;ref可以包装任意类型(包括基本类型),返回的是带有.value属性的 RefImpl 实例。对象用reactive,基本类型用ref;如果解构reactive对象会丢失响应性,需要用toRefs转换。

写在最后

Vue 的响应式系统,本质上就是“数据劫持 + 发布-订阅模式”

  • 数据劫持:Vue 2 用Object.defineProperty,Vue 3 用Proxy,拦截数据的读和写

  • 依赖收集:在数据被读取时(getter),把当前正在执行的 Watcher/Effect 记录下来

  • 派发更新:在数据被修改时(setter),通知所有依赖它的 Watcher/Effect 重新执行

双向绑定则是在这个基础上,加上了 DOM 事件监听(@input),让视图的变化能反向修改数据。

理解了这套机制,你就能明白为什么Vue.set存在、为什么数组有些操作不触发更新、为什么ref要用.value访问——所有 API 设计背后,都有源码层面的必然逻辑

(PS:本文由deepseek整理生成)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/25 11:58:52

模板驱动型文档自动化:零代码实现精准合规交付

1. 项目概述&#xff1a;当文档生产变成“填空游戏”&#xff0c;我们到底省下了什么&#xff1f;你有没有过这种体验&#xff1a;每周一早上&#xff0c;雷打不动地打开Word&#xff0c;复制上一份合同模板&#xff0c;把客户名字、金额、日期挨个替换成新的&#xff0c;再检查…

作者头像 李华
网站建设 2026/6/25 11:57:44

LPC213x UART0串口通信:从波特率计算到中断FIFO实战指南

1. LPC213x UART0&#xff1a;从手册到实战的深度解析在嵌入式开发中&#xff0c;串口&#xff08;UART&#xff09;几乎是工程师的“老朋友”。无论是打印调试信息、与上位机通信&#xff0c;还是连接GPS、蓝牙模块&#xff0c;都离不开它。NXP&#xff08;原飞利浦半导体&…

作者头像 李华
网站建设 2026/6/25 11:53:43

C++文件流模板:通用数组读写技巧

template <class T> void input(T arr[], int n, ifstream& in) {for (int i 0; i < n; i) {in >> arr[i];} }读入作用从文件输入流 in 中&#xff0c;读取 n 个数据&#xff0c;依次存入数组 arr。逐点说明template <class T>&#xff1a;声明这是函…

作者头像 李华
网站建设 2026/6/25 11:52:08

面试辅助工具横评:我试了5款AI面试工具,最后留下了OfferGo

上半年跳槽&#xff0c;面了十几家公司。说句实话&#xff0c;不是能力不行&#xff0c;是面试现场太容易崩了。 明明准备了一周&#xff0c;面试官换个问法脑子就一片白。面完之后那个懊悔——其实我会的。 后来开始试市面上的AI面试辅助工具。前前后后装了5款&#xff0c;踩…

作者头像 李华
网站建设 2026/6/24 23:18:26

Deep-Live-Cam实时换脸部署全指南:CUDA、ONNX与可信计算基实战

1. 这不是“又一个AI玩具”&#xff0c;而是实时换脸技术落地的分水岭 你刷到过那个视频吗&#xff1f;主播对着镜头眨眼&#xff0c;下一秒整张脸就变成了《速度与激情》里的多米尼克托莱多&#xff0c;连嘴角抽动的节奏都严丝合缝——没有延迟、没有卡顿、连背景虚化都跟着人…

作者头像 李华
网站建设 2026/6/24 23:09:26

Codex本地AI引擎安装配置全指南:WSL路径、沙箱策略与VS Code集成

1. 这不是“又一个AI插件”&#xff1a;Codex到底在帮你解决什么真问题&#xff1f; 很多人看到“Codex安装教程”第一反应是&#xff1a;“哦&#xff0c;又一个让VS Code变聪明的AI插件&#xff1f;”——这种理解偏差&#xff0c;恰恰是新手踩坑的第一步。Codex不是ChatGPT的…

作者头像 李华