在 Vue 2 中,双向绑定是 Vue 的核心功能之一,它通过数据响应式系统使得数据的变化自动反映在视图上,同时用户在视图上做的更改也能够同步回数据模型。这种双向绑定是通过 数据劫持(Data Hijacking) 和 发布-订阅模式(Publish-Subscribe Pattern) 实现的。以下是双向绑定原理及实现方式的详细解析:
1. 数据劫持(Data Hijacking)
Vue 2 使用 Object.defineProperty()
来实现数据劫持。对于每一个数据属性,Vue 会将它转化为一个 getter
和 setter
,从而拦截对数据的访问和修改操作。
实现原理:
-
当访问一个数据属性时,
getter
会被调用,Vue 可以通过这个getter
来跟踪这个属性依赖的所有视图组件。 -
当修改一个数据属性时,
setter
会被调用,Vue 可以通知所有依赖这个属性的视图组件进行更新。
// 定义一个函数,用于将对象的某个属性转换为响应式属性
function defineReactive(obj, key, val) {
// 使用 Object.defineProperty 对 obj 对象的 key 属性进行拦截
Object.defineProperty(obj, key, {
// 这个属性是否可以被枚举,例如在 for...in 循环中是否能遍历到
enumerable: true,
// 这个属性是否可以被删除或再次修改属性描述符
configurable: true,
// getter:当外部访问 obj.key 时,会调用此方法
get() {
console.log(`访问属性:${key}`);
return val; // 返回属性的当前值
},
// setter:当外部修改 obj.key 时,会调用此方法
set(newVal) {
if (newVal !== val) { // 检查新值是否与旧值不同
console.log(`属性${key}从${val}变为${newVal}`);
val = newVal; // 更新属性的值
// 通知机制可以放在这里,告知相关视图进行更新
}
}
});
}
解释:
-
defineReactive
函数:用于将对象的某个属性(如key
)转化为响应式属性。它接收三个参数:对象obj
,属性名key
,以及属性的初始值val
。 -
Object.defineProperty()
:这是 JavaScript 中用于定义或修改对象属性的 API。在这里,Vue 用它来拦截对象属性的访问和赋值操作。 -
enumerable
和configurable
:这两个选项分别控制属性是否可枚举和是否可配置。将enumerable
设置为true
,意味着该属性可以被循环枚举;configurable: true
允许属性描述符被修改或属性被删除。 -
getter
方法:当外部代码访问obj.key
时,getter
会被调用。getter
会返回属性的当前值val
,并在调试时打印访问日志。 -
setter
方法:当外部代码为obj.key
赋新值时,setter
会被调用。setter
首先检查新值是否与旧值不同,如果不同则更新属性的值,并可以在此处触发视图更新逻辑。
2. 发布-订阅模式(Publish-Subscribe Pattern)
在 Vue 2 中,视图和数据之间的关联是通过一个叫做 Watcher
的类来实现的。Watcher
作为发布者和订阅者之间的桥梁,每一个 Watcher
订阅了某个数据属性,当该属性发生变化时,Watcher
就会被通知并触发相应的视图更新。
实现步骤:
-
Dep(依赖收集器):每个数据属性都有一个对应的
Dep
对象,用来收集所有依赖于该属性的Watcher
。 -
Watcher:当视图模板被解析时,Vue 会为每一个依赖数据的地方创建一个
Watcher
,并将其添加到对应数据属性的Dep
中。 -
通知更新:当数据属性发生变化时,对应的
Dep
会通知所有相关的Watcher
,进而触发视图更新。
// Dep 类用于收集依赖,每个属性都对应一个 Dep 实例
class Dep {
constructor() {
this.subs = []; // 存储所有依赖于这个属性的 Watcher 实例
}
// 添加依赖的方法,传入一个 Watcher 实例
addSub(sub) {
this.subs.push(sub);
}
// 通知所有依赖,触发它们的更新
notify() {
this.subs.forEach(sub => sub.update());
}
}
// Watcher 类,用于当数据变化时执行特定的回调函数
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm; // 保存组件实例
this.getter = expOrFn; // 将表达式或函数保存起来
this.cb = cb; // 当数据变化时执行的回调函数
this.value = this.get(); // 初始化时获取并保存属性值,并触发依赖收集
}
// 获取属性值并进行依赖收集
get() {
Dep.target = this; // 将当前的 Watcher 实例指向 Dep.target
let value = this.getter.call(this.vm, this.vm); // 调用表达式或函数获取值
Dep.target = null; // 释放 Dep.target,避免重复收集
return value; // 返回获取到的属性值
}
// 当数据变化时,Watcher 会调用这个方法来更新视图
update() {
const oldValue = this.value; // 保存旧值
this.value = this.get(); // 获取新值并重新依赖收集
this.cb.call(this.vm, this.value, oldValue); // 执行回调函数,传入新旧值
}
}
解释:
-
Dep
类:每个属性对应一个Dep
实例,用于收集所有依赖于该属性的Watcher
。subs
数组用于存储这些Watcher
实例。 -
addSub
方法:将一个Watcher
实例添加到subs
数组中,这是依赖收集的过程。 -
notify
方法:当属性值发生变化时,调用notify
方法遍历subs
数组,并触发每个Watcher
的update
方法,从而通知视图更新。 -
Watcher
类:用于创建一个观察者实例。当依赖的数据发生变化时,Watcher
会被通知并执行相应的更新操作。Watcher
保存了一个回调函数cb
,用于在数据变化时执行。 -
get
方法:当创建Watcher
实例时,会调用get
方法获取数据的初始值,并触发依赖收集。Dep.target
用于临时存储当前的Watcher
实例,以便在getter
中添加依赖。 -
update
方法:当数据变化时,update
方法会被调用,它会重新获取数据的当前值,并调用回调函数来更新视图。
3. 双向绑定的完整流程
结合以上内容,Vue 2 的双向绑定机制大致可以描述为以下步骤:
-
数据劫持:通过
Object.defineProperty()
拦截对象属性的访问和修改,将属性转化为响应式属性。 -
依赖收集:在初始化组件时,Vue 解析模板并为每个依赖于响应式属性的地方创建一个
Watcher
。这些Watcher
会被添加到相应属性的Dep
中。 -
数据变更通知:当响应式属性的值发生变化时,
setter
被触发,对应的Dep
通知所有依赖此属性的Watcher
,调用它们的update
方法更新视图。