- 定义
- 观察者模式
- 手写实现
- vue中如何利用订阅发布模式实现数据双向绑定
- vue中的$on, $emit eventBus等
- Promise
定义
订阅发布模式(Publish-Subscribe Pattern),又称观察者模式(Observer Pattern),是一种常用的设计模式,用于解决多个对象间的依赖关系,降低系统耦合度,提高代码的复用性和可维护性。
发布订阅是一种消息范式,发布者不会将消息直接发送给订阅者,而是将发布的消息分为不同的类别,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。
在订阅发布模式中,存在两类角色:发布者和订阅者。发布者负责向订阅者发送消息,而订阅者则负责接收消息并做出相应的处理。发布者和订阅者之间通过一个中介者(通常称为消息队列或事件总线)来进行通信,发布者发布消息后,消息将被发送到中介者,中介者再将消息分发给所有订阅者。
具体实现上,订阅者需要先订阅发布者感兴趣的事件或主题(Topic),这样才能接收到相应的消息。当发布者发布与该主题相关的消息时,所有订阅该主题的订阅者将收到该消息,并进行相应的处理。
观察者模式
观察者模式是行为模式之一,它的作用是当一个对象的状态发生变化时,能够自动通知其他关联对象,自动刷新对象状态,观察者模式提供给关联对象一种同步通信的手段,使某个对象与依赖它的其他对象之间保持状态同步。
区别
订阅发布模式和观察者模式都是常见的设计模式,它们的作用都是在对象之间建立松耦合的联系,实现对象间的通信。不过两者之间也有不同之处:
-
观察者模式中,被观察者(也叫主题)维护了一个观察者列表,当主题状态发生变化时,会遍历观察者列表,依次通知观察者执行相应的方法,实现对象间的通信。而订阅发布模式中,则是通过一个事件中心来协调发布者和订阅者之间的关系。发布者负责发布事件,订阅者则订阅事件,当事件触发时,订阅者就会收到通知。
-
观察者模式中,观察者需要直接订阅被观察者,这会造成双方之间的耦合。而订阅发布模式中,发布者和订阅者之间不需要直接联系,通过事件中心来进行交互,实现了更加松散的耦合。
-
在观察者模式中,被观察者和观察者之间是一对多的关系,一个被观察者可以有多个观察者。而在订阅发布模式中,发布者和订阅者之间是多对多的关系,一个发布者可以有多个订阅者,一个订阅者也可以订阅多个发布者。
-
在观察者模式中,观察者收到通知后,必须按照被观察者规定的方法进行操作,这限制了观察者的自由度。而在订阅发布模式中,发布者只需要发布事件,订阅者则可以根据自己的需求进行操作,实现了更加灵活的设计。
总的来说,观察者模式适合在一对多的关系中使用,比如 GUI 控件和模型之间的关系,而订阅发布模式则适用于多对多的关系,比如消息通知、事件驱动等场景。
手写实现
class PubSub {
constructor() {
this.subscribers = {};
}
subscribe(event, callback) {
if (!this.subscribers[event]) {
this.subscribers[event] = [];
}
this.subscribers[event].push(callback);
}
publish(event, data) {
if (!this.subscribers[event]) {
return;
}
this.subscribers[event].forEach((callback) => callback(data));
}
}
// 使用示例
const pubsub = new PubSub();
// 订阅事件
pubsub.subscribe('myEvent', (data) => {
console.log('收到 myEvent 事件', data);
});
// 发布事件
pubsub.publish('myEvent', { message: 'Hello, world!' });
在这个例子中,我们定义了一个 PubSub 类,它有两个方法 subscribe 和 publish。subscribe 方法用于订阅事件,接受两个参数,第一个是事件名称,第二个是回调函数,该回调函数会在事件发生时被调用。publish 方法用于发布事件,接受两个参数,第一个是事件名称,第二个是事件数据,它会调用所有订阅该事件的回调函数,并将事件数据作为参数传递给它们。
在上面的示例中,我们首先创建了一个 PubSub 实例,并使用 subscribe 方法订阅了一个名为 myEvent 的事件。接着,我们使用 publish 方法发布了该事件,并传递了一个包含消息的数据对象。在事件发生时,subscribe 方法中定义的回调函数会被调用,打印出事件数据。
vue中的双向数据绑定
vue通过数据劫持和订阅发布模式实现了双向数据绑定。具体来说,当一个 Vue 实例创建时,Vue 会遍历 data 选项中的所有属性,使用 Object.defineProperty 将它们转化为 getter/setter,并且在内部建立依赖收集的关系。
当数据发生变化时,Vue 会通过发布-订阅模式通知所有相关的观察者(Watcher),让它们更新视图。Vue 中的观察者实现了一个 update() 方法,在被通知更新时会调用这个方法去更新对应的视图。
在视图中,当用户对数据进行修改时,Vue 会利用 v-model 指令实现双向数据绑定。具体来说,v-model 会监听 input 或者其他表单元素的 input 或者 change 事件,然后通过事件回调函数将修改后的值赋值给对应的 data 属性。
因此,Vue 的双向数据绑定的实现原理可以概括为以下几个步骤:
- 遍历 data 选项中的属性,利用 Object.defineProperty 转化为 getter/setter,并在内部建立依赖收集的关系;
- 当数据发生变化时,Vue 会通知所有相关的观察者(Watcher)更新视图;
- 在视图中,v-model 指令会监听表单元素的 input 或者 change 事件,并通过事件回调函数将修改后的值赋值给对应的 data 属性。
通过以上步骤,Vue 实现了数据的双向绑定,让开发者可以方便地实现视图与数据的同步更新。
vue中的$on和$emit
在Vue中,$on和$emit是实现订阅发布模式的核心方法。$on用于订阅事件,$emit用于触发事件。
- $on的实现:
当执行vm.$on(eventName, callback)时,会执行以下代码:
if (Array.isArray(eventName)) {
for (let i = 0, l = eventName.length; i < l; i++) {
this.$on(eventName[i], callback)
}
} else {
(this._events[eventName] || (this._events[eventName] = [])).push(callback)
}
可以看到,$on方法内部实现了事件订阅的逻辑。首先判断传入的eventName是否为数组,如果是,则递归调用$on方法,将每一个eventName和callback分别传入,实现一次性订阅多个事件的功能。如果eventName不是数组,则将callback加入到_events对象的eventName属性值对应的数组中。
$emit的实现:
当执行vm.$emit(eventName, arg1, arg2, …)时,会执行以下代码:
let cbs = this._events[eventName]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(this, args)
}
}
可以看到,$emit方法内部实现了事件发布的逻辑。首先从_events对象中获取eventName对应的回调函数数组cbs,如果存在cbs,则将其转换为数组形式。然后,将除eventName外的其他参数组成一个数组args。最后,遍历cbs数组,依次执行每一个回调函数,并将args作为参数传入。
可以看出,$on和$emit的实现都是通过对_events对象的处理来实现的,也就是说,Vue中的$on和$emit确实是使用了订阅发布模式。
Promise
Promise 内部使用发布订阅模式来处理异步操作的状态变化。
在 Promise 内部,有三种状态:pending(等待中)、fulfilled(已成功)和 rejected(已失败)。当状态发生变化时,Promise 对象会发布对应的事件,通知所有注册的回调函数。这些回调函数就是订阅者。
当 Promise 对象处于 pending 状态时,它会维护一个回调函数列表。当 Promise 对象的状态发生变化时,会依次执行这个回调函数列表中的函数。这些回调函数就是发布者。
例如,当 Promise 对象状态变为 fulfilled(已成功)时,它会执行 resolve 函数,并向所有订阅了该 Promise 对象的回调函数传递成功的结果。这个过程中,Promise 内部就使用了发布订阅模式。
下面是一个示例代码,用于演示 Promise 内部使用发布订阅模式的过程:
class MyPromise {
constructor() {
this.state = 'pending';
this.callbacks = [];
}
resolve(result) {
this.state = 'fulfilled';
this.result = result;
this.callbacks.forEach(callback => callback(this.result));
}
reject(error) {
this.state = 'rejected';
this.error = error;
this.callbacks.forEach(callback => callback(this.error));
}
then(callback) {
if (this.state === 'fulfilled') {
callback(this.result);
} else if (this.state === 'pending') {
this.callbacks.push(callback);
}
}
}
const promise = new MyPromise();
promise.then(result => console.log(result));
promise.resolve('Hello World!');
在上面的示例代码中,MyPromise 类实现了一个简单的 Promise 对象。它维护了 Promise 对象的状态和回调函数列表,当 Promise 对象状态发生变化时,会执行对应的回调函数列表中的函数。在 then 方法中,如果 Promise 对象已经处于 fulfilled 状态,直接执行回调函数;如果处于 pending 状态,将回调函数添加到回调函数列表中。
在执行 promise.resolve(‘Hello World!’); 时,Promise 对象状态变为 fulfilled,并执行回调函数列表中的回调函数,输出结果 Hello World!。在这个过程中,Promise 内部使用了发布订阅模式来处理状态变化和回调函数的执行。
扩展:
promise实现发布订阅模式
具体实现方式是,使用 Promise 的 then 方法来订阅事件,当 Promise 状态改变时,会自动触发 then 方法中注册的回调函数。而使用 Promise 的 resolve 方法来触发事件,resolve 方法会将 Promise 的状态改变为 resolved,从而触发 then 方法中注册的回调函数。
举个例子,我们可以创建一个名为 EventBus 的类,内部包含一个 Promise 对象:
class EventBus {
constructor() {
this.promise = Promise.resolve();
}
publish(event, data) {
this.promise = this.promise.then(() => {
const handlers = this.events[event] || [];
handlers.forEach(handler => handler(data));
});
}
subscribe(event, handler) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(handler);
}
unsubscribe(event, handler) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(h => h !== handler);
}
}
}
在上面的代码中,我们定义了一个 EventBus 类,包含了三个方法:publish、subscribe 和 unsubscribe。其中,subscribe 方法用来订阅事件,unsubscribe 方法用来取消订阅事件,而 publish 方法用来触发事件。
具体来说,publish 方法会将事件名和数据作为参数,然后将其包装成一个 Promise,并将其添加到 promise 链中。在添加到链中之前,会先执行链中的所有前置任务(即在该任务之前添加到链中的所有任务),以保证事件的触发顺序。
而 subscribe 方法则会将事件名和回调函数添加到 this.events 对象中,而 unsubscribe 方法则会将指定的回调函数从事件列表中删除。
下面是一个使用 EventBus 的例子:
const eventBus = new EventBus();
eventBus.subscribe('event1', data => {
console.log(`Event1: ${data}`);
});
eventBus.subscribe('event2', data => {
console.log(`Event2: ${data}`);
});
eventBus.publish('event1', 'Hello, world!');
eventBus.publish('event2', 'Hi there!');
在上面的代码中,我们首先创建了一个 EventBus 对象,然后分别订阅了 event1 和 event2 事件,并分别传入了回调函数。最后,我们通过 publish 方法来触发这两个事件,并传入了不同的数据。当事件触发时,会自动调用对应的回调函数,并将数据作为参数传入。
文章中若有不恰当的地方或者有什么需要改进的地方,可以在下方评论,感谢