Vue 的 双向绑定(v-model) 核心是 "数据响应式 + 视图事件监听"的联动机制 ------本质是语法糖,底层通过「数据劫持」监听数据变化,同步更新视图;同时通过「事件监听」捕获视图操作(如输入框输入),反向同步更新数据,最终实现 数据 ↔ 视图 双向自动同步。
不同 Vue 版本的实现原理有差异,以下分 Vue 2.x 和 Vue 3.x 详细说明(面试高频考点):
一、核心概念铺垫
双向绑定的核心依赖 3 个核心模块,无论 Vue 2 还是 3 都离不开这个逻辑:
- 响应式数据:对数据进行"劫持",数据变化时能主动触发更新;
- 视图更新:数据变化后,自动找到依赖该数据的 DOM 并更新;
- 事件监听:监听视图的用户操作(如 input 输入、select 选择),将操作结果同步回数据。
二、Vue 2.x 双向绑定实现原理(Object.defineProperty)
Vue 2 核心通过 Object.defineProperty 劫持数据的 getter/setter,配合「依赖收集」和「发布-订阅模式」实现双向绑定,具体流程如下:
1. 核心步骤拆解
(1)数据劫持:Object.defineProperty
Vue 2 会对组件 data 中的数据进行递归遍历,给每个属性通过 Object.defineProperty 重写 getter 和 setter:
- getter :当数据被访问时(如视图渲染用到
{{ msg }}),触发 getter,进行「依赖收集」(记录当前组件的 Watcher); - setter :当数据被修改时(如
this.msg = 'new'),触发 setter,通知所有依赖该数据的 Watcher 执行更新。
示例代码(简化版):
javascript
function defineReactive(obj, key, value) {
// 递归处理嵌套对象(如 data: { user: { name: 'xxx' } })
observe(value);
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get() {
// 依赖收集:记录当前 Watcher
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
observe(newValue); // 新值是对象时,继续劫持
dep.notify(); // 发布通知:触发所有依赖的 Watcher 更新
}
});
}
// 递归劫持 data 所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
new Observer(obj);
}
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
// 处理数组:重写 push/pop/splice 等方法(Vue 2 数组劫持特殊处理)
this.observeArray(obj);
} else {
// 处理对象:遍历属性劫持
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
observeArray(arr) {
arr.forEach(item => observe(item));
}
}
(2)依赖收集:Dep 类(发布者)
每个响应式属性都会对应一个 Dep 实例(发布者),用于管理依赖该属性的所有 Watcher(订阅者):
addSub(watcher):添加订阅者(Watcher);notify():发布更新通知,触发所有订阅者的update方法。
perl
class Dep {
constructor() {
this.subs = []; // 存储所有依赖的 Watcher
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => sub.update()); // 通知所有 Watcher 更新
}
}
(3)视图更新:Watcher 类(订阅者)
Watcher 是连接数据和视图的桥梁,每个组件对应一个 Watcher(或多个):
- 初始化时,会触发数据的
getter,将自身添加到Dep的订阅列表; - 当数据变化触发
Dep.notify()时,Watcher的update方法会被调用,最终触发组件的重新渲染(render函数)。
(4)双向绑定:v-model 语法糖
v-model 本质是 :value(数据 → 视图)和 @input(视图 → 数据)的语法糖,例如:
xml
<!-- 等价于 -->
<input v-model="msg" />
<input :value="msg" @input="msg = $event.target.value" />
- 数据 → 视图:
msg变化时,通过响应式机制触发input元素的value更新; - 视图 → 数据:用户输入时,触发
input事件,将输入值赋值给msg,触发msg的setter,完成数据同步。
2. Vue 2 双向绑定完整流程
- 组件初始化时,
data被observe劫持所有属性的getter/setter; - 组件渲染时,触发数据的
getter,将组件的Watcher添加到Dep订阅列表(依赖收集); - 数据变化时,触发
setter→Dep.notify()→ 所有依赖的Watcher.update()→ 组件重新渲染(数据 → 视图); - 视图操作(如输入框输入)触发
input事件 → 赋值给数据(this.msg = 新值)→ 触发setter→ 重复步骤 3(视图 → 数据)。
3. Vue 2 双向绑定的局限性
- 无法劫持数组的索引修改(如
arr[0] = 1)和长度修改(如arr.length = 0),需通过 Vue 提供的$set或数组方法(push/splice等)触发更新; - 无法劫持对象的新增属性(如
this.user.age = 18),需通过this.$set(this.user, 'age', 18)添加响应式属性; Object.defineProperty需递归遍历对象,性能开销较大(尤其深层嵌套对象)。
三、Vue 3.x 双向绑定实现原理(Proxy + Reflect)
Vue 3 废弃了 Object.defineProperty,改用 Proxy 代理数据 + Reflect 反射操作,解决了 Vue 2 的局限性,同时性能更优,具体流程如下:
1. 核心改进点
- Proxy 优势:
- 可直接代理整个对象(无需递归遍历属性),新增属性自动响应;
- 支持代理数组的索引修改、长度修改(如
arr[0] = 1、arr.length = 0); - 支持 13 种拦截操作(如
get、set、deleteProperty等),功能更强大。
- Reflect 作用:
- 统一对象操作的返回值(如
Reflect.set成功返回true,失败返回false); - 避免直接操作对象的副作用(如
delete obj.key会报错,Reflect.deleteProperty返回布尔值); - 与 Proxy 拦截方法一一对应,便于代码统一管理。
2. 核心步骤拆解
(1)数据代理:Proxy + reactive
Vue 3 中通过 reactive 函数创建对象的 Proxy 代理,递归处理嵌套对象(仅在访问嵌套对象时才代理,懒加载优化):
javascript
function reactive(obj) {
// 仅代理对象/数组(基础类型用 ref 处理)
if (typeof obj !== 'object' || obj === null) return obj;
// 创建 Proxy 代理
return new Proxy(obj, {
// 拦截属性访问(getter)
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
// 依赖收集:与 Vue 2 Dep 类似,记录 Watcher
track(target, key);
// 递归代理嵌套对象(懒加载)
return isObject(result) ? reactive(result) : result;
},
// 拦截属性修改/新增(setter)
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const success = Reflect.set(target, key, value, receiver);
// 只有值变化时才触发更新
if (oldValue !== value && success) {
// 发布通知:触发依赖更新
trigger(target, key);
}
return success;
},
// 拦截属性删除
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key);
if (success) {
trigger(target, key);
}
return success;
}
});
}
// 判断是否为对象/数组
function isObject(value) {
return typeof value === 'object' && value !== null;
}
(2)基础类型响应式:ref
Proxy 无法代理基础类型(如 string、number),Vue 3 用 ref 包装基础类型,通过 value 属性访问/修改:
csharp
function ref(value) {
// 创建包含 value 属性的对象
const refObj = {
get value() {
track(refObj, 'value'); // 依赖收集
return value;
},
set value(newValue) {
if (newValue === value) return;
value = newValue;
trigger(refObj, 'value'); // 发布通知
}
};
return refObj;
}
(3)依赖收集与更新:track + trigger
Vue 3 用 track 替代 Vue 2 的 Dep.addSub,用 trigger 替代 Dep.notify,逻辑更简洁:
track(target, key):在get拦截时调用,记录"目标对象 + 属性"对应的依赖(Watcher);trigger(target, key):在set/deleteProperty拦截时调用,触发"目标对象 + 属性"的所有依赖更新。
(4)双向绑定:v-model 语法糖(兼容 Vue 2,新增优化)
Vue 3 的 v-model 仍为语法糖,但支持更多场景:
- 普通输入框:
v-model="msg"→:modelValue="msg" @update:modelValue="msg = $event"(Vue 3 统一了自定义组件的绑定逻辑); - 自定义组件:无需再区分
props和$emit,直接通过v-model绑定,支持多个v-model(如v-model:name="name" v-model:age="age")。
示例(自定义组件双向绑定):
xml
<!-- 父组件 -->
<Child v-model:name="name" v-model:age="age" />
<!-- 子组件 -->
<template>
<input :value="name" @input="$emit('update:name', $event.target.value)" />
<input :value="age" @input="$emit('update:age', $event.target.value)" />
</template>
<script setup>
const props = defineProps(['name', 'age']);
</script>
3. Vue 3 双向绑定完整流程
- 调用
reactive/ref对数据进行 Proxy 代理; - 组件渲染时,访问数据触发
Proxy.get→ 调用track收集依赖(记录 Watcher); - 数据变化时(如
obj.key = 新值、arr[0] = 新值),触发Proxy.set→ 调用trigger通知依赖更新 → 组件重新渲染(数据 → 视图); - 视图操作触发
update:modelValue事件(或input事件)→ 赋值给代理数据 → 触发Proxy.set→ 重复步骤 3(视图 → 数据)。
四、Vue 2 vs Vue 3 双向绑定核心差异
| 对比维度 | Vue 2.x | Vue 3.x |
|---|---|---|
| 核心 API | Object.defineProperty |
Proxy + Reflect |
| 数据劫持范围 | 仅属性,需递归遍历 | 整个对象,懒加载代理嵌套对象 |
| 数组支持 | 不支持索引/长度修改,需特殊处理 | 原生支持索引/长度修改 |
| 对象新增属性 | 需 $set 手动添加响应式 |
自动响应新增属性 |
| 基础类型响应式 | 需嵌套在对象中(如 data: { num: 0 }) |
直接用 ref 包装(如 const num = ref(0)) |
| 性能 | 递归遍历开销大,深层对象性能差 | 懒加载代理,性能更优 |
五、面试必背总结
- 双向绑定本质 :
数据响应式(数据→视图) + 事件监听(视图→数据)的语法糖(v-model); - Vue 2 核心 :
Object.defineProperty劫持 getter/setter + Dep(发布者)+ Watcher(订阅者),局限性是不支持数组索引/对象新增属性; - Vue 3 核心 :
Proxy + Reflect代理数据 + track(依赖收集)+ trigger(更新触发),解决 Vue 2 局限性,性能更优; - v-model 原理 :Vue 2 是
:value + @input,Vue 3 是:modelValue + @update:modelValue,支持多字段绑定。
掌握以上核心逻辑,就能清晰回答 Vue 双向绑定的实现原理,同时覆盖版本差异(面试高频考点)。