原理
简单来说Vue 通过数据劫持 结合发布者-订阅者模式实现了双向绑定
如何理解
先来理解数据劫持
数据劫持
数据劫持是指在访问或修改对象属性时,通过拦截的方式进行处理。在前端框架中,特别是在实现数据绑定的过程中,数据劫持是一种常见的技术,它可以用于监听对象属性的变化并触发相应的操作,比如更新视图。
Vue 中通过数据劫持实现了双向绑定,主要是通过 Object.defineProperty 来实现的。
Object.defineProperty方法
概念 Object.defineProperty 是 JavaScript 中用于定义对象属性的方法。它允许你精确控制属性的行为,包括读取(get)、写入(set)和删除(delete)等。具体内容可以查看 参考文档 。
代码
javascript
Object.defineProperty(obj, prop, descriptor)
// 详细写法
Object.defineProperty(obj, 'propertyName', {
get: function() {
// 在读取属性时调用
return this._internalValue; // 返回属性的实际值
},
set: function(value) {
// 在设置属性时调用
this._internalValue = value; // 更新属性的实际值
},
//value 和 get/set 只能存在一个
//value: '666' // 属性值
writable: true, // 是否可写
enumerable: true, // 是否可枚举
configurable: true // 是否可配置
});
参数
- obj:要定义属性的对象。
- prop:要定义或修改的属性的名称。
- descriptor:要定义或修改的属性描述符。
其中属性描述符 descriptor 是一个对象,它可以包含以下属性:
- value: 属性的值,默认为 undefined。
- writable: 布尔值,表示属性值是否可写 ,默认为 false。如果为 false,属性值不能被重新赋值。
- enumerable: 布尔值,表示属性是否可枚举,默认为 false。如果为 false,属性将不会出现在 for...in 或 Object.keys() 的遍历中。
- configurable: 布尔值,表示属性是否可删除或是否可以修改属性的特性,默认为 false。如果为 false,任何尝试删除属性或修改属性特性的操作都会被忽略。且不能再把该属性变回数据属性。
另外,descriptor 还可以包含 get 和 set 方法,用于获取 和设置属性值。这两个方法分别在访问属性和修改属性时被调用。
示例
javascript
let obj = {};
Object.defineProperty(obj, 'name', {
value: '张三',
writable: false, // 不能重新赋值
enumerable: true,
configurable: true
});
console.log(obj.name); // 输出: 张三
obj.name = '李四'; // 由于 writable 为 false,此赋值无效
for (let key in obj) {
console.log(key); // 输出: name
}
delete obj.name; // 由于 configurable 为 true,属性可删除
console.log(obj.name); // 输出: undefined
Object.defineProperty 在 Vue 中的应用是通过它来设置响应式数据的 getter 和 setter,从而实现数据的双向绑定。Vue 利用它来劫持对象的属性,实现对数据的监听和更新。
再来理解发布者-订阅者模式
发布者-订阅者模式
概念
发布者-订阅者模式(Publisher-Subscriber Pattern)是一种设计模式,也被称为观察者模式(Observer Pattern)。它是一种行为型模式,用于定义对象之间的一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知并自动更新。
在这个模式中,有两个主要角色:
- 发布者(Publisher): 也称为主题(Subject),负责维护一组订阅者并通知它们状态的变化。发布者是被观察的对象,当其状态发生变化时,会通知所有订阅者。
- 订阅者(Subscriber): 也称为观察者(Observer),监听发布者的变化并作出相应的响应。订阅者通过订阅发布者来接收通知,以便在发布者状态变化时执行相应的操作。
基本流程如下:
- 发布者维护一个订阅者列表。
- 订阅者通过订阅发布者来注册自己。
- 当发布者的状态发生变化时,会遍历订阅者列表,并调用它们的更新方法(通知)。
- 订阅者接收到通知后执行相应的操作。
这种模式的优点在于降低了对象之间的耦合度,使得发布者和订阅者之间可以独立地进行扩展和修改。这也是为什么这个模式常用于实现事件处理系统、UI组件之间的通信等场景。
在JavaScript中,发布者-订阅者模式通常通过自定义事件来实现。例如,DOM事件就是一种发布者-订阅者模式,通过addEventListener注册事件监听器,通过dispatchEvent触发事件,实现了对象之间的解耦。在现代JavaScript框架和库中,也经常使用这种模式来实现组件之间的通信
代码示例
发布者(Publisher)
发布者是一个对象,负责维护一组订阅者,并在自身状态变化时通知订阅
javascript
// 观察者模式中的发布者(Publisher)类
class Publisher {
constructor() {
// 存储订阅者列表
this.subscribers = [];
}
// 添加订阅者
subscribe(subscriber) {
this.subscribers.push(subscriber);
}
// 移除订阅者
unsubscribe(subscriber) {
this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
}
// 通知所有订阅者
notify(message) {
this.subscribers.forEach(subscriber => {
subscriber.update(message);
});
}
}
订阅者(Subscriber)
订阅者是一个对象,它订阅发布者的事件或状态变化,并在发布者通知时执行相应的操作
javascript
// 观察者模式中的订阅者(Observer)类
class Subscriber {
// 订阅者的更新操作
update(message) {
console.log(`收到消息: ${message}`);
}
}
使用
javascript
// 创建发布者实例
const publisher = new Publisher();
// 创建订阅者实例
const subscriberA = new Subscriber();
const subscriberB = new Subscriber();
// 将订阅者订阅到发布者
publisher.subscribe(subscriberA);
publisher.subscribe(subscriberB);
// 模拟发布者状态变化时通知订阅者
publisher.notify('你好');
输出结果
javascript
// 输出结果
// 收到消息:你好
// 收到消息:你好
Publisher 发布消息时,所有订阅者(Subscriber)都收到了相同的消息,并执行了各自的 update 方法,输出了接收到的消息。
现在我们了解了数据劫持 和 发布者-订阅者模式的概念,再来看vue是如何实现的。
Vue实现步骤
下面是一个简化实现案例
1.对数据进行监控
对需要观察者(observe)的数据进行递归遍历,包括子属性都加上setter和getter
javascript
// 观察者
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
// 递归遍历对象,将每个属性都转换为响应式
walk(obj) {
Object.keys(obj).forEach(key => {
this.defineReactive(obj, key, obj[key]);
});
}
// 将对象的属性转换为响应式
defineReactive(obj, key, val) {
const dep = new Dep();
// 递归处理嵌套属性
observe(val);
// 添加setter和getter
Object.defineProperty(obj, key, {
get() {
// 在读取属性时,将当前 Watcher 添加到依赖收集器中
if (Dep.target) {
dep.addSubscriber(Dep.target);
}
return val;
},
set(newVal) {
if (val !== newVal) {
val = newVal;
// 当数据发生变化时,通知依赖收集器中的所有 Watcher 更新
dep.notify();
}
}
});
}
2.解析模板指令
Compile(编译)解析模板指令,然后调用 updateText 方法更新文本节点的内容,将模板变量替换成数据
javascript
// 简化版编译器部分内容
...
// 编译文本节点,处理 {{...}} 表达式
compileText(node) {
// 提取表达式内容
const exp = RegExp.$1;
// 更新文本节点的内容
this.updateText(node, exp);
...
}
...
// 更新文本节点的内容
updateText(node, exp) {
// 获取表达式的值,并设置文本节点的内容
const value = this.vm[exp];
node.textContent = value;
}
...
3.Watcher创建
在模板编译阶段,对于每个需要建立双向绑定的表单元素,Vue 会创建一个 Watcher 实例,将其与数据进行关连
javascript
// 简化版编译器部分内容
...
// 编译文本节点,处理 {{...}} 表达式
compileText(node) {
// 提取表达式内容
const exp = RegExp.$1;
// 更新文本节点的内容
this.updateText(node, exp);
...
// 创建 Watcher 实例,负责更新视图
new Watcher(this.vm, exp, (newVal) => {
// 表达式的值变化时,更新文本节点的内容
node.textContent = newVal;
});
...
}
...
Watcher 是 Vue 数据响应系统的核心组件,它在数据发生变化时负责通知相关的视图进行更新。
javascript
// 简化版Watcher订阅者
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 将当前 Watcher 实例指定为全局的当前 Watcher
Dep.target = this;
// 触发一次属性的 getter,从而在依赖收集阶段将当前 Watcher 添加到依赖列表中
this.vm[this.key];
// 重置全局的当前 Watcher
Dep.target = null;
}
// 更新视图
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
Watcher(订阅者)是Observer(观察者)和Compile(编译器)之间通讯的桥梁,Vue 中的响应式系统通过 Watcher、Dep(属性订阅器)等机制来建立数据与视图之间的关联,实现了数据驱动视图的目标。主要步骤是:
- 在自身实例化时往属性订阅器(dep)里添加自己
javascript
class Dep {
constructor() {
this.subscribers = [];
}
// 添加订阅者(Watcher)
addSubscriber(subscriber) {
this.subscribers.push(subscriber);
}
// 通知所有订阅者更新
notify() {
this.subscribers.forEach(subscriber => subscriber.update());
}
}
Dep 的主要作用是用于管理一组依赖(Watcher 实例),在数据变化时通知这些依赖执行相应的更新操作。
- 自身中要有一个update()方法
javascript
// 简化版Watcher订阅者
class Watcher {
...
// 更新视图
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
...
}
- 等到属性变动dep.notice通知的时候,能调用自身的update()方法,并触发observer中绑定的回调
javascript
// 数据劫持,进行通知
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
...
},
set(newVal) {
if (val !== newVal) {
val = newVal;
// 当数据发生变化时,通知依赖收集器中的所有 Watcher 更新
dep.notify();
}
}
});
}
javascript
// 调用update()方法
class Dep {
...
// 通知所有订阅者更新
notify() {
this.subscribers.forEach(subscriber => subscriber.update());
}
...
}
javascript
class Watcher {
...
// 更新视图
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
javascript
// 创建 Watcher 实例,负责更新视图
const watcher = new Watcher(vm, 'message', (newVal) => {
console.log('视图更新了:', newVal);
});
总的来说,Vue 数据双向绑定的核心是通过 Watcher 实现数据与视图的同步。当数据变化时,通过 Watcher 检测到并通知相关视图更新;当用户与视图交互时,通过 Watcher 更新相关的数据。这样就实现了数据和视图的双向绑定。
4.以MVVM为数据入口
作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到了双向绑定的效果
MVVM(Model-View-ViewModel)是一种软件架构模式,其中 Model 表示应用程序的数据模型,View 表示用户界面,而 ViewModel 则充当 Model 和 View 之间的中介,处理数据的交互和业务逻辑。在前端框架中,MVVM 常常用于实现数据绑定,其中 Observer(观察者)用于监测数据变化,Compile(编译器)用于解析模板并创建 Watcher(观察者),Watcher 负责更新视图。