为了更深入地理解Vue.js的双向绑定原理,我们可以分别用代码示例来说明Vue 2.x中使用Object.defineProperty
和Vue 3.x中使用Proxy
实现响应式数据的方式,以及如何通过这些机制实现双向绑定。
Vue 2.x 使用 Object.defineProperty
Vue 2.x通过Object.defineProperty
方法拦截对象属性的getter和setter,从而实现数据的响应式。当数据变化时,视图自动更新;当用户输入时,数据也会更新,实现双向绑定。
以下是简化版的Vue 2.x响应式系统的实现:
javascript
function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
// 创建一个Dep实例,用于收集依赖
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.depend(); // 依赖收集
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 通知依赖更新
}
});
}
// 递归遍历对象,使其每个属性都为响应式
function observe(value) {
if (typeof value !== 'object') return;
Object.keys(value).forEach(key => defineReactive(value, key, value[key]));
}
// 简化版Dep类
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if (Dep.target) {
this.subscribers.push(Dep.target);
}
}
notify() {
this.subscribers.forEach(sub => sub.update());
}
}
// 模拟Watcher
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = obj[key]; // 触发getter进行依赖收集
Dep.target = null;
}
update() {
this.value = this.obj[this.key];
this.cb(this.value);
}
}
const data = { price: 5, quantity: 2 };
observe(data);
// 创建Watcher实例,当数据变化时更新视图
new Watcher(data, 'price', (val) => {
console.log(`Price changed to ${val}`);
});
// 改变数据,触发更新
data.price = 20;
Vue 3.x 使用 Proxy
Vue 3.x使用Proxy
来实现响应式系统,它可以监听对象的所有操作,比Object.defineProperty
有更好的性能和灵活性。
以下是简化版的Vue 3.x响应式系统的实现:
javascript
function reactive(target) {
const handler = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
}
};
return new Proxy(target, handler);
}
let activeEffect = null;
function track(target, key) {
// 省略依赖收集实现
}
function trigger(target, key) {
// 省略触发更新实现
}
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
const product = reactive({ price: 5, quantity: 2 });
effect(() => {
console.log(`Total is ${product.price * product.quantity}`);
});
product.price = 20;
双向绑定的实现
在Vue中,v-model
指令用于在表单输入和应用状态之间创建双向绑定。对于普通的文本输入,Vue内部大致做了如下事情:
- 绑定一个
input
事件监听器到输入框上,当用户输入时,更新相应的数据。 - 使用
Object.defineProperty
或Proxy
对数据进行响应式处理,当数据变化时,更新输入框的值。
通过这种方式,Vue实现了数据和视图之间的双向绑定,让开发者能够以声明式的方式处理用户输入和应用状态的同步。
让我们逐步混合并详细解释上面的代码示例,以便更好地理解Vue的响应式系统和双向绑定是如何工作的。这里我们主要聚焦于Vue 2.x的实现方式,因为它依赖于Object.defineProperty
,这有助于理解基础的响应式原理。Vue 3.x采用Proxy
实现了更高级和高效的响应式系统,但基本原理保持一致。
Vue 2.x 响应式系统基础
1. defineReactive
函数
defineReactive
函数是响应式系统的核心。它接受一个对象obj
,一个键key
和该键的初始值val
。该函数的目的是将obj[key]
转换成getter/setter,以便Vue能够在访问或修改该属性时执行自定义逻辑。
- Getter:当属性被访问时,getter会执行。在这个示例中,getter的作用是进行依赖收集。每当Vue组件或计算属性访问这个属性时,它都会被添加到当前属性的依赖列表中。这样,当属性值变化时,Vue就知道哪些组件需要重新渲染。
- Setter:当属性被修改时,setter会执行。在这个示例中,setter的作用是触发更新。它会检查新值是否与旧值不同,如果不同,则通知所有依赖于该属性的地方,告诉它们值已经改变。
2. observe
函数
observe
函数递归地遍历对象的所有属性,对每个属性调用defineReactive
函数,使整个对象变为响应式。这意味着Vue可以监听对象内部任何级别的数据变化。
3. Dep
类
Dep
类是一个简单的依赖管理器。它收集一个属性的所有依赖(即观察该属性的组件和计算属性),并在该属性变化时通知它们。每个属性都有自己的Dep
实例。
- depend方法用于添加一个新的依赖到当前的依赖列表中。
- notify方法用于当属性变化时,通知所有依赖进行更新。
4. Watcher
类
Watcher
类模拟Vue中的观察者。它的构造函数接收一个对象、一个键和一个回调函数。创建Watcher
实例时,它会读取指定的属性,这个读取操作会触发属性的getter,从而进行依赖收集。当属性值变化时,setter会触发dep.notify()
,然后Watcher
的update
方法被调用,执行回调函数,通常是更新视图的操作。
运行过程
- 初始化 :通过调用
observe(data)
,数据对象data
的每个属性都被转化为响应式,可以被Vue追踪变化。 - 依赖收集 :当
new Watcher
实例创建时,它尝试读取data.price
,这个操作触发data.price
的getter。在getter中,依赖收集发生,当前的Watcher
实例被添加到data.price
的依赖列表中。 - 响应更新 :当执行
data.price = 20;
时,data.price
的setter被触发,因为新值与旧值不同,它调用dep.notify()
,通知所有依赖(即之前收集的Watcher
实例)data.price
已经改变。Watcher
的update
方法被调用,执行回调函数,通常会导致视图更新。
通过这种机制,Vue实现了数据的响应式和双向绑定:数据变化自动更新视图,用户输入(通过如v-model
指令绑定的视图变化)自动更新数据。
让我们详细解释上面提供的两个示例的运行过程:
Vue 2.x 示例解释
-
初始化响应式系统:
defineReactive
函数被调用,为对象data
的每个属性(在这个例子中是price
和quantity
)创建getter和setter,实现数据的响应式。 -
依赖收集:
- 当创建
Watcher
实例时,它的构造函数会读取data.price
,这会触发price
的getter。 - 在getter内部,
dep.depend()
被调用,将当前的Watcher
实例添加到Dep
实例的subscribers
数组中,完成依赖收集。 - 这意味着
price
属性现在有了一个订阅者(依赖),即Watcher
实例。
- 当创建
-
响应数据变化:
- 当执行
data.price = 20;
时,price
属性的setter被触发。 - 在setter内部,
dep.notify()
被调用,遍历Dep
实例的subscribers
数组,并调用每个订阅者的update
方法。 Watcher
的update
方法会重新读取price
属性的值,并执行回调函数,打印新的价格。
- 当执行
Vue 3.x 示例解释
-
使用
Proxy
创建响应式对象:reactive
函数通过返回一个Proxy
实例使product
对象变得响应式。这个Proxy
能够拦截对product
对象的所有操作。 -
依赖收集:
effect
函数接受一个函数作为参数并立即执行它。在这个函数内部,当访问product.price
和product.quantity
时,触发了Proxy
的get
陷阱。- 在
get
陷阱内部,track
函数被调用以进行依赖收集。虽然这里没有详细展示track
函数的实现,但其基本作用是记录哪个effect
依赖于哪些属性。
-
响应数据变化:
- 当更改
product.price
的值时,触发了Proxy
的set
陷阱。 - 在
set
陷阱内部,首先通过Reflect.set
完成属性值的更新,然后调用trigger
函数来响应这个变化。 trigger
函数会查找所有依赖于被更改属性的effect
函数,并重新执行它们。在这个例子中,重新执行导致打印出新的总价。
- 当更改
总结
- 在Vue 2.x中,
Object.defineProperty
用于拦截对象属性的访问和修改,通过Dep
和Watcher
类进行依赖收集和更新通知。 - 在Vue 3.x中,
Proxy
提供了一种更强大和灵活的方式来监听对象的活动。它允许Vue拦截对象上的任何操作,而effect
函数和依赖跟踪/触发机制用于响应数据变化。 - 在双向绑定中,用户界面的更改(如输入框的输入)会更新响应式数据,而响应式数据的更改又会更新用户界面,形成一个闭环。