Vue2 和 Vue3 的数据双向绑定(核心是响应式系统),本质都是通过「数据劫持 / 监听」+「依赖收集」+「触发更新」实现,但在底层原理、实现方式、性能表现上存在核心差异,且 Vue3 针对 Vue2 的缺陷做了大幅优化。
一、先明确:双向绑定的基础是「响应式系统」
数据双向绑定是「视图更新 → 数据更新」+「数据更新 → 视图更新」的双向联动:
- 视图→数据:通过
v-model(本质是v-bind绑定值 +v-on监听输入事件)实现,Vue2 和 Vue3 此部分逻辑基本一致; - 数据→视图:通过响应式系统实现(核心差异点),即数据变化时自动通知视图重新渲染,这是下文重点讲解的内容。
二、核心区别对比表
| 对比维度 | Vue2(响应式) | Vue3(响应式) |
|---|---|---|
| 核心原理 | Object.defineProperty(对象属性劫持) | Proxy(对象代理) |
| 监听目标 | 对象的单个属性 | 整个对象 / 数组(代理整个目标对象) |
| 数组支持 | 仅支持 7 种数组方法(push/pop 等),无法监听数组索引修改、长度修改 | 原生支持数组所有操作(索引修改、长度修改、方法调用),无需特殊处理 |
| 对象支持 | 无法监听对象新增属性 、删除属性(需用 Vue.set/Vue.delete) | 原生支持对象新增属性、删除属性,无需额外 API |
| 依赖收集 | 绑定在属性的 get 方法中,组件级依赖收集 |
基于 Proxy 代理,配合 effect 实现更精细的依赖收集(可实现局部更新) |
| 性能表现 | 初始化时递归遍历对象所有属性,劫持每个属性的 get/set,大型对象性能较差 |
非侵入式代理,初始化无需递归遍历所有属性(懒加载特性),大型对象 / 复杂场景性能更优 |
| 兼容性 | 支持 IE10 及以上(兼容旧浏览器) | 不支持 IE 浏览器(Proxy 无 ES5 兼容方案) |
| 额外 API 依赖 | 依赖 Vue.set/Vue.delete/$set/$delete 处理新增 / 删除属性 |
无需额外 API,直接操作对象 / 数组即可 |
三、分别详解:原理与实现
1. Vue2 数据双向绑定原理与缺陷
(1)核心原理:Object.defineProperty 劫持属性
Vue2 通过 Object.defineProperty 对数据对象的每个属性 进行劫持,重写其 get(取值)和 set(赋值)方法,配合「发布 - 订阅模式」实现响应式:
- 初始化劫持 :递归遍历
data中的所有对象属性,对每个属性调用Object.defineProperty,重写get和set; - 依赖收集(get 方法) :当属性被模板渲染 / 计算属性使用时,
get方法会触发依赖收集,将当前组件的渲染函数(Watcher)添加到该属性的「依赖列表」中; - 触发更新(set 方法) :当属性值被修改时,
set方法会触发通知,遍历该属性的「依赖列表」,调用所有 Watcher 的更新方法,重新渲染视图; - 数组特殊处理 :由于
Object.defineProperty无法监听数组索引和长度变化,Vue2 重写了数组的 7 个可变方法(push、pop、shift、unshift、splice、sort、reverse),在方法内部触发更新通知。
(2)核心代码简化演示(Vue2 响应式核心)
javascript
// 模拟 Vue2 响应式实现
function defineReactive(obj, key, value) {
// 递归劫持子对象属性
if (typeof value === 'object' && value !== null) {
observe(value);
}
// 劫持当前属性的 get/set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 取值时:依赖收集
get() {
console.log(`收集 ${key} 属性的依赖`);
// 此处简化:实际会将 Watcher 加入依赖列表
return value;
},
// 赋值时:触发更新
set(newValue) {
if (newValue === value) return;
console.log(`修改 ${key} 属性,触发视图更新`);
// 新值如果是对象,需要递归劫持
if (typeof newValue === 'object' && newValue !== null) {
observe(newValue);
}
value = newValue;
// 此处简化:实际会遍历依赖列表,调用 Watcher.update()
}
});
}
// 递归遍历对象,劫持所有属性
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
// 遍历对象属性
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 测试
const data = { name: '张三', age: 20, hobbies: ['篮球', '游戏'] };
observe(data);
// 正常触发更新
data.name = '李四'; // 输出:修改 name 属性,触发视图更新
// 无法监听对象新增属性
data.gender = '男'; // 无输出,视图不会更新(需用 Vue.set)
// 无法监听数组索引修改
data.hobbies[0] = '足球'; // 无输出,视图不会更新
// 支持重写后的数组方法
data.hobbies.push('跑步'); // 输出:修改 hobbies 属性,触发视图更新(Vue2 重写了 push 方法)
(3)Vue2 响应式的核心缺陷
- 无法监听对象新增属性 和删除属性 :例如
data.gender = '男'不会触发更新,需手动调用this.$set(data, 'gender', '男')/this.$delete(data, 'name'); - 无法监听数组索引修改 和长度修改 :例如
arr[0] = 10、arr.length = 0不会触发更新,需用splice替代; - 初始化性能差:递归遍历所有属性并劫持
get/set,当数据对象庞大时,初始化耗时较长; - 侵入式劫持:直接修改原对象的属性描述符,对原对象有侵入性。
2. Vue3 数据双向绑定原理与优化
(1)核心原理:Proxy 代理 + effect 副作用
Vue3 放弃了 Object.defineProperty,改用 ES6 新增的 Proxy 对数据对象进行整体代理 ,配合 effect(副作用函数)实现响应式,解决了 Vue2 的所有缺陷:
- 整体代理 :
Proxy直接代理整个目标对象,而非单个属性,无需递归遍历所有属性(初始化性能更优); - 依赖收集(effect) :当响应式数据被
effect函数使用时,会自动收集依赖,建立「数据 - effect」的映射关系; - 触发更新 :当响应式数据被修改时,会自动触发对应的
effect函数(如组件渲染函数、计算属性),实现视图更新; - 原生支持对象 / 数组 :
Proxy能监听对象的新增 / 删除属性、数组的索引 / 长度修改、数组方法调用,无需特殊处理和额外 API。
(2)核心代码简化演示(Vue3 响应式核心)
javascript
// 模拟 Vue3 响应式实现(简化版)
const targetMap = new WeakMap(); // 存储:目标对象 -> 属性 -> effect 列表
let activeEffect = null; // 存储当前活跃的 effect 函数
// effect:副作用函数,依赖响应式数据
function effect(fn) {
activeEffect = fn;
fn(); // 执行函数,触发 Proxy get,收集依赖
activeEffect = null;
}
// 收集依赖
function track(target, key) {
if (!activeEffect) return;
// 获取目标对象的依赖映射
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取属性的 effect 列表
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 添加当前 effect 到列表
deps.add(activeEffect);
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
// 执行所有相关的 effect 函数
deps.forEach(effect => effect());
}
}
// 创建响应式对象(Proxy 代理)
function reactive(target) {
return new Proxy(target, {
// 取值时:收集依赖
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
// 深层响应式:懒加载(访问时才代理子对象)
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
// 赋值时:触发更新
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
},
// 删除属性时:触发更新
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
// 测试
const data = reactive({ name: '张三', age: 20, hobbies: ['篮球', '游戏'] });
// effect 监听数据变化(对应组件渲染函数)
effect(() => {
console.log(`视图更新:${data.name} - ${data.age} - ${data.hobbies}`);
});
// 正常触发更新
data.name = '李四'; // 输出:视图更新:李四 - 20 - 篮球,游戏
// 原生支持对象新增属性
data.gender = '男'; // 输出:视图更新:李四 - 20 - 篮球,游戏,男
// 原生支持对象删除属性
delete data.age; // 输出:视图更新:李四 - undefined - 篮球,游戏,男
// 原生支持数组索引修改
data.hobbies[0] = '足球'; // 输出:视图更新:李四 - undefined - 足球,游戏,男
// 原生支持数组长度修改
data.hobbies.length = 2; // 输出:视图更新:李四 - undefined - 足球,游戏
(3)Vue3 响应式的核心优化
- 原生支持对象新增 / 删除属性 :
Proxy的set/deleteProperty陷阱能监听对象属性的新增和删除,无需Vue.set/Vue.delete; - 原生支持数组所有操作 :
Proxy能监听数组的索引修改、长度修改、所有方法调用,解决了 Vue2 数组监听的缺陷; - 懒加载特性:初始化时无需递归遍历所有属性,仅在访问子对象时才进行代理(深层响应式懒加载),大幅提升大型对象的初始化性能;
- 非侵入式代理 :
Proxy不会修改原对象,而是返回一个代理对象,对原对象无侵入性,更安全; - 更精细的依赖收集 :基于
effect实现,可实现局部更新,减少不必要的渲染,性能更优。
四、v-model 的细微区别(双向绑定的视图层差异)
除了响应式系统的核心差异,Vue2 和 Vue3 的 v-model 也有细微区别(视图→数据的绑定逻辑):
- 组件上的
v-model:- Vue2:默认绑定
value属性 +input事件,如需自定义,需通过model选项配置(props: ['value'], model: { prop: 'value', event: 'input' }); - Vue3:默认绑定
modelValue属性 +update:modelValue事件,支持多个v-model绑定(无需.sync 修饰符),更灵活;
- Vue2:默认绑定
- 修饰符支持 :Vue3 新增了更多
v-model修饰符(如.trim、.number之外的.lazy优化),且支持自定义修饰符。
五、总结
- 核心原理差异 :Vue2 用
Object.defineProperty(劫持单个属性),Vue3 用Proxy(代理整个对象); - 关键功能差异 :Vue3 原生支持对象新增 / 删除属性、数组所有操作,无需额外 API,Vue2 需依赖
Vue.set/Vue.delete; - 性能差异:Vue3 初始化性能更优(懒加载),大型场景表现更好,Vue2 初始化递归劫持属性,性能较差;
- 兼容性差异:Vue2 支持 IE10+,Vue3 不支持 IE(Proxy 无兼容方案);
v-model差异 :组件上的默认绑定属性 / 事件不同,Vue3 支持多v-model绑定,更灵活。