Vue双向绑定原理

1. Vue的双向数据绑定机制

Vue 的双向数据绑定主要依赖于两个核心的技术:

  • 数据劫持(Data Hijacking) :Vue 通过 Object.defineProperty 劫持对象的属性,使得属性值的获取和设置可以被拦截,进而进行一些自定义的操作。
  • 发布-订阅模式(Observer-Pattern) :当数据发生变化时,通知视图更新。Vue 中的 Watcher 充当了这个角色,负责监听数据变化并进行视图更新。

2. 数据劫持:Object.defineProperty

Vue 的数据绑定的实现原理首先需要理解 Vue 如何“劫持”数据对象的属性。这通过 Object.defineProperty
来实现,它使得我们可以在对象的每个属性上定义自定义的 gettersetter 方法。

1. 数据劫持的过程

在 Vue 的响应式系统中,Vue 会使用 Object.defineProperty 来遍历每个数据对象的属性,并为每个属性定义 getter
setter。这个过程让 Vue 能够“劫持”数据的访问和修改,从而在数据发生变化时,自动触发视图的更新。

例如,假设有一个数据对象 data,Vue 会将对象的每个属性劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let data = { message: 'Hello Vue!' };

// Vue 对 data.message 做了劫持
Object.defineProperty(data, 'message', {
get() {
// 收集依赖(即 Watcher)
return this._message;
},
set(newValue) {
// 更新数据时,通知视图更新
this._message = newValue;
// 通知所有的依赖 Watcher,触发视图更新
updateView();
}
});

此时,data.message 的访问就变成了通过 gettersetter 完成。getter 会让 Vue
收集依赖,setter 会触发视图更新。

2. 递归遍历嵌套对象

如果数据对象是嵌套对象,Vue 会递归地为对象中的每一层数据做同样的处理。因此,Vue 也能监听到嵌套对象的变化。比如:

1
2
3
4
5
6
7
8
9
10
11
let data = {
user: {
name: 'Wang',
age: 30
}
};

// Vue 会递归地劫持 `user` 对象中的 `name` 和 `age`
Object.defineProperty(data.user, 'name', { /* getter 和 setter */ });
Object.defineProperty(data.user, 'age', { /* getter 和 setter */ });

3. 依赖收集:Dep 和 Watcher

Vue 的数据绑定不仅仅是数据的“劫持”,它还需要实现依赖收集和通知机制。这个机制是通过 DepWatcher 完成的。

1. Dep:依赖收集器

每个响应式数据属性都有一个 Dep 对象,Dep 负责管理所有依赖于该数据的 Watcher。每当某个数据的 getter 被访问时,Vue
会将当前的 Watcher 添加到 Dep 中,作为该数据的依赖。

简单来说,Dep 就是一个容器,存储着所有对某个数据有依赖关系的观察者(即 Watcher)。每当数据发生变化时,Dep 会通知所有的
Watcher,从而触发视图更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    
// Dep 负责收集依赖
class Dep {
constructor() {
this.subscribers = [];
}

// 添加 Watcher
addSub(watcher) {
this.subscribers.push(watcher);
}

// 通知所有 Watcher 更新
notify() {
this.subscribers.forEach(watcher => watcher.update());
}
}

2. Watcher:视图更新监听器

Watcher 是一个用来观察数据变化的类。每当数据发生变化时,Watcher 会收到通知,进而更新视图。Watcher 通过 Dep
来收集依赖,绑定了具体的视图更新逻辑。

每当 data.messagesetter 被触发时,Dep 会通知相关的 Watcher 执行视图更新。Watcher
会执行回调,通常会重新渲染视图或更新界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Watcher 负责视图更新

class Watcher {
constructor(vm, expression, callback) {
this.vm = vm;
this.expression = expression;
this.callback = callback;

// 在构造时,收集依赖(即访问数据触发 getter)
this.value = this.get();
}

get() {
// 执行 getter,收集依赖
return this.vm[this.expression];
}

update() {
// 数据变化时触发视图更新
const newValue = this.get();
this.callback(newValue);
}
}

4. 发布-订阅模式

DepWatcher 一起实现了发布-订阅模式:

  • 发布者Dep 是发布者,它会维护一组 Watcher
  • 订阅者Watcher 是订阅者,关注某个数据的变化。
  • 通知 :当数据变化时,Dep 会通知所有的 Watcher,让它们进行视图更新。

5. 双向数据绑定:v-model

Vue 实现双向数据绑定(Two-way Data Binding)最常见的方式是通过 v-model 指令,特别是在表单控件(如
<input>)中。

v-model 实际上是对数据绑定和事件绑定的组合:

  • 数据绑定(value:将模型数据绑定到视图(如 <input>value)。
  • 事件绑定(input:当用户在输入框中输入内容时,通过 input 事件更新模型数据。

例如:

1
<input v-model="message" />

在背后,v-model 会做两件事情:

  1. 数据绑定 :它将 message 的值与输入框的 value 绑定起来。每当 message 的值发生变化时,输入框的内容会自动更新。
  2. 事件监听 :它会监听输入框的 input 事件,并在用户输入时更新 message 的值。这样就实现了视图到数据的双向绑定。

Vue 实现 v-model 时,会在 input 元素上绑定 value 属性和 input 事件:

<input :value="message" @input="message = $event.target.value" />

这样,当用户输入时,message 会被更新,进而触发 messagesetter,再通过 Dep 通知所有依赖的 Watcher
更新视图。

6. 总结

Vue 的双向数据绑定通过以下几个核心机制实现:

  1. 数据劫持 :Vue 通过 Object.defineProperty 来劫持对象的属性,在获取和修改数据时做一些自定义操作。
  2. 依赖收集 :Vue 使用 Dep 来管理依赖,将依赖的数据和 Watcher 进行绑定。
  3. 发布-订阅模式 :当数据发生变化时,Dep 会通知所有依赖它的 Watcher,从而触发视图更新。
  4. 双向数据绑定 :通过 v-model,Vue 使得数据和视图之间的绑定是双向的,用户的输入会更新数据,数据的变化也会更新视图。

这种双向数据绑定的实现让开发者能够更加专注于业务逻辑,而不必关注手动更新 DOM,提高了开发效率并减少了出错的可能性。