章
目
录
双向数据绑定是Vue的重要功能之一,它让数据与视图之间的交互变得便捷,今天,咱们就来深入探讨一下Vue2和Vue3在双向数据绑定原理与实现上的差异。
一、双向数据绑定原理
Vue实现双向数据绑定的核心依托于MVVM模式,该模式由三部分构成:
- Model(数据层):在Vue里,它主要负责存储数据,是应用程序中数据的载体。比如说,一个待办事项列表应用中,待办事项的内容、完成状态等数据就存储在Model中。
- View(视图层):这部分由HTML模板和Vue指令组成,也就是用户直接看到并与之交互的界面。用户在页面上看到的待办事项的展示、输入框等都属于View层。
- ViewModel(业务逻辑层):它是Vue的核心部分,作为一个Vue实例,就像一座桥梁,连接着Model和View。当Model中的数据发生变化时,ViewModel能够监听到这些变化,并通过相应机制更新View;反之,当用户在View上进行操作(如输入内容、点击按钮等),ViewModel也能捕获这些事件,并将变化同步到Model中。
在双向数据绑定的过程中,当数据发生变化时,监听器(Observer)会触发依赖通知,从而实现对视图层的更新;而当用户操作视图时,解析器(Compiler)会捕获相关事件(例如v-model
指令下的输入操作),进而更新数据层。
二、双向数据绑定的实现方式
(一)监听器的实现
在Vue中,监听器用于监听数据变化,主要有数据劫持和发布 – 订阅模式这两种实现方式。
- 数据劫持
- Vue2的实现:Vue2利用
Object.defineProperty
方法来实现数据劫持。这个方法能够拦截对象属性的getter
和setter
操作。在getter
中,会收集依赖,也就是记录哪些地方使用了这个数据;而在数据变化,即setter
被调用时,通过触发之前收集依赖过程中记录的更新函数,来实现视图的更新。下面通过一个简单示例来理解:
- Vue2的实现:Vue2利用
//示例
const o = {};
let bValue = 38;
Object.defineProperty(o, "b", {
get() {
//收集依赖
return bValue;
},
set(newValue) {
bValue = newValue;
// 触发视图更新
},
});
在这个例子里,定义了一个对象o
,并通过Object.defineProperty
对其属性b
进行劫持。当获取b
的值时,会执行get
函数,在这里可以进行依赖收集;当设置b
的值时,set
函数被调用,除了更新数据bValue
,还可以在这个函数里添加触发视图更新的逻辑。
– Vue3的实现:Vue3则使用Proxy
来替代Object.defineProperty
。Proxy
可以创建一个对象的代理,通过它能够方便地对对象的基本操作(如获取属性、设置属性等)进行拦截和自定义。当调用代理对象的get
方法时,会进行依赖收集;调用set
方法时,就会进行依赖更新。具体语法为const p = new Proxy(target, handler)
,其中target
是要使用Proxy
包装的目标对象,可以是各种类型的对象,包括数组、函数等;handler
是一个包含各种函数的对象,这些函数定义了代理在执行不同操作时的行为。示例如下:
//示例
let products = new Proxy(
{
browsers: ["Internet Explorer", "Netscape"],
},
{
get: function (obj, prop) {
// 收集依赖
return obj[prop];
},
set: function (obj, prop, value) {
obj[prop] = value;
// 触发视图更新,表示成功
return true;
},
},
);
在这个示例中,创建了一个products
的代理对象。当访问products
的属性时,get
函数会被调用进行依赖收集;当设置属性值时,set
函数被调用,更新对象属性的同时触发视图更新。
2. 发布 – 订阅模式
Vue.js借助发布 – 订阅模式来管理组件和数据之间的依赖关系。简单来说,当数据发生变化时,依赖于这些数据的视图(可以理解为观察者)会收到通知并进行更新。下面通过一段代码来详细了解:
// 1. 发布者类
class Dep {
constructor() {
this.subscribers = [];
}
// 依赖收集
depend() {
if (Dep.target &&!this.subscribers.includes(Dep.target)) {
this.subscribers.push(Dep.target);
}
}
// 通知更新
notify() {
this.subscribers.forEach(sub => sub());
}
}
// 2.
// ①数据劫持 Vue2-Start
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
dep.depend(); // 收集依赖
return val;
},
set(newVal) {
val = newVal;
dep.notify(); // 触发更新
}
});
}
// Vue2-End
// ②响应式代理 Vue3-Start
function reactive(obj) {
const deps = new Map(); // 存储每个属性的依赖
return new Proxy(obj, {
get(target, key) {
let dep = deps.get(key);
if (!dep) {
dep = new Dep();
deps.set(key, dep);
}
if (Dep.currentEffect) {
dep.depend(Dep.currentEffect); // 收集依赖
}
return target[key];
},
set(target, key, value) {
target[key] = value;
deps.get(key)?.notify(); // 触发更新
return true;
}
});
}
// Vue3-End
// 3. 观察者函数
function watch(effect) {
Dep.target = effect; // 标记当前依赖
effect(); // 首次执行以触发getter
Dep.target = null; // 重置
}
// 使用示例
const data = {};
defineReactive(data, 'count', 0);
// 订阅数据变化
watch(() => {
console.log('Count updated:', data.count);
});
// 触发更新
data.count = 1; // 输出: "Count updated: 1"
在这段代码中,Dep
类充当发布者,它维护了一个订阅者数组subscribers
。depend
方法用于收集依赖,将相关的观察者添加到数组中;notify
方法则用于通知所有订阅者进行更新。defineReactive
函数是Vue2中结合发布 – 订阅模式实现数据劫持的关键,在get
和set
操作中分别进行依赖收集和更新通知。Vue3中的reactive
函数通过Proxy
和Dep
实现了类似的功能,不过在依赖管理上使用了Map
来存储每个属性的依赖。watch
函数则是一个简单的观察者函数示例,用于订阅数据变化并执行相应操作。
(二)Compile模板解析
Compile主要负责实现视图到数据的绑定,其过程分为以下两个阶段:
- 解析阶段:遍历DOM树,识别各种Vue指令,比如
v-model
、{{}}
插值等。针对每个指令,创建对应的更新函数,并将其绑定到相应的数据依赖上。例如,下面这个处理v-model
指令的函数:
function compileInput(node, data, key) {
node.value = data[key]; // 初始化值
node.addEventListener('input', (e) => {
data[key] = e.target.value; // View → Model
});
// 订阅数据变化,更新视图
watch(key, (value) => node.value = value); // Model → View
}
在这个函数里,首先将数据绑定到输入框的初始值。然后,通过监听输入框的input
事件,当用户在输入框中输入内容时,将输入的值同步到数据层(View → Model
)。同时,通过watch
函数订阅数据变化,当数据发生改变时,更新输入框的显示内容(Model → View
)。
2. 虚拟DOM优化:Vue会将模板转换为轻量级的虚拟DOM树。当数据发生变化时,会生成新的虚拟DOM,然后通过Diff算法比对新旧虚拟DOM的差异,只对真实DOM中发生变化的部分进行更新。这样做可以大大减少对真实DOM的操作次数,提高页面的性能和渲染效率。
通过对Vue2和Vue3双向数据绑定原理与实现的深入分析,我们可以看到这两个版本在技术实现上的演进和优化。理解这些差异,有助于我们在实际项目中更好地选择和运用Vue框架,大家还是要掌握下为好哦。