在 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
方法更新视图。
总结
Vue 2 的双向绑定通过数据劫持和发布-订阅模式实现了数据和视图的自动同步,Object.defineProperty
提供了数据响应性,而 Watcher
和 Dep
通过依赖收集和通知机制保证了视图与数据的联动性。这种机制的实现虽然强大,但由于 Object.defineProperty
的限制,对对象属性的劫持只能在初始化时完成,这也是为什么 Vue 3 转向了使用 Proxy 来实现更灵活的响应式系统。