定义:订阅发布模式定义了对象间一种一对多的关系,让多个观察者对象同时监听同一主题对象,当一个对象改变时,所有依赖于它的对象都将得到通知。
优点:
- 支持单一广播通信,当对象的状态发生变化时,会自动通知订阅它的对象。
- 发布者与订阅者的耦合性降低,发布者只关注发布的消息,订阅者只关注订阅的消息
缺点
- 消耗一定的时间和内存
- 可以弱化对象之间的联系,但是过度使用会造成代码难以维护和理解。
核心:订阅者、发布者
1、实现原理:
- 确定谁是发布者
- 给发布者增加一个缓存列表,用来缓存回调函数来通知订阅者
- 最后就是发布消息,发布者遍历这个列表,依次触发里面存放的订阅者的回调函数,来通知订阅者。
下面是一个简单的订阅发布的js示例
var shoeObj = {}; // 定义发布者
shoeObj.list = []; // 缓存列表 存放订阅者回调函数
// 增加订阅者
shoeObj.listen = function(fn) {
shoeObj.list.push(fn); // 订阅消息添加到缓存列表
}
// 发布消息
shoeObj.trigger = function(){
for(var i = 0,fn; fn = this.list[i++];) {
fn.apply(this,arguments);
}
}
// 小红订阅如下消息
shoeObj.listen(function(color,size){
console.log("颜色是:"+color);
console.log("尺码是:"+size);
});
// 小花订阅如下消息
shoeObj.listen(function(color,size){
console.log("再次打印颜色是:"+color);
console.log("再次打印尺码是:"+size);
});
shoeObj.trigger("红色",40);
shoeObj.trigger("黑色",42);
// 颜色是:红色
// 尺码是:40
// 再次打印颜色是:红色
// 再次打印尺码是:40
// 颜色是:黑色
// 尺码是:42
// 再次打印颜色是:黑色
// 再次打印尺码是:42
2、源码中的发布-订阅模式**
发布-订阅模式在源码中应用很多,特别是现在很多前端框架都会有的双向绑定机制的场景,这里以现在很火的
Vue
为例,来分析一下Vue
是如何利用发布-订阅模式来实现视图层和数据层的双向绑定。先借用官网的双向绑定原理图:
下面稍微解释一下这个图(框架源码整个过程比较复杂,如果现在看不懂下面几段也没关系,大致了解一下即可)。
组件渲染函数(Component Render Function)被执行前,会对数据层的数据进行响应式化。响应式化大致就是使用
Object.defineProperty
把数据转为getter/setter
,并为每个数据添加一个订阅者列表的过程。这个列表是getter
闭包中的属性,将会记录所有依赖这个数据的组件。
- 也就是说,响应式化后的数据相当于发布者。
- 每个组件都对应一个
Watcher
订阅者。当每个组件的渲染函数被执行时,都会将本组件的Watcher
放到自己所依赖的响应式数据的订阅者列表里,这就相当于完成了订阅,一般这个过程被称为依赖收集(Dependency Collect
)。 - 组件渲染函数执行的结果是生成虚拟
DOM
树(Virtual DOM Tree
),这个树生成后将被映射为浏览器上的真实的 DOM 树,也就是用户所看到的页面视图。 - 当响应式数据发生变化的时候,也就是触发了
setter
时,setter
会负责通知(Notify
)该数据的订阅者列表里的Watcher
,Watcher
会触发组件重渲染(Trigger re-render
)来更新(update
)视图。
我们可以看看 Vue 的源码:
// src/core/observer/index.js 响应式化过程
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
const value = getter ? getter.call(obj) : val // 如果原本对象拥有getter方法则执行
dep.depend() // 进行依赖收集,dep.addSub
return value
},
set: function reactiveSetter(newVal) {
// ...
if (setter) { setter.call(obj, newVal) } // 如果原本对象拥有setter方法则执行
dep.notify() // 如果发生变更,则通知更新
}
})
而这个
dep
上的depend
和notify
就是订阅和发布通知的具体方法。
- 简单来说,响应式数据是消息的发布者,而视图层是消息的订阅者,如果数据更新了,那么发布者会发布数据更新的消息来通知视图更新,从而实现数据层和视图层的双向绑定。
3、发布-订阅模式的优缺点
发布-订阅模式最大的优点就是解耦:
- 时间上的解耦 :注册的订阅行为由消息的发布方来决定何时调用,订阅者不用持续关注,当消息发生时发布者会负责通知;
- 对象上的解耦 :发布者不用提前知道消息的接受者是谁,发布者只需要遍历处理所有订阅该消息类型的订阅者发送消息即可(迭代器模式),由此解耦了发布者和订阅者之间的联系,互不持有,都依赖于抽象,不再依赖于具体;
- 由于它的解耦特性,发布-订阅模式的使用场景一般是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变。发布-订阅模式还可以帮助实现一些其他的模式,比如中介者模式。
发布-订阅模式也有缺点:
- 增加消耗 :创建结构和缓存订阅者这两个过程需要消耗计算和内存资源,即使订阅后始终没有触发,订阅者也会始终存在于内存;
- 增加复杂度 :订阅者被缓存在一起,如果多个订阅者和发布者层层嵌套,那么程序将变得难以追踪和调试,参考一下 Vue 调试的时候你点开原型链时看到的那堆 deps/subs/watchers 们…
- 缺点主要在于理解成本、运行效率、资源消耗,特别是在多级发布-订阅时,情况会变得更复杂。
4、发布订阅模式与其他设计模式的区别
发布-订阅模式和观察者模式
观察者模式与发布-订阅者模式,在平时你可以认为他们是一个东西,但是某些场合(比如面试)下可能需要稍加注意,借用网上一张流行的图:
区别主要在发布-订阅模式中间的这个 Event Channel:
- 观察者模式 中的观察者和被观察者之间还存在耦合,被观察者还是知道观察者的;
- 发布-订阅模式 中的发布者和订阅者不需要知道对方的存在,他们通过消息代理来进行通信,解耦更加彻底;
发布-订阅模式和责任链模式
发布-订阅模式和责任链模式也有点类似,主要区别在于:
- 发布-订阅模式 传播的消息是根据需要随时发生变化,是发布者和订阅者之间约定的结构,在多级发布-订阅的场景下,消息可能完全不一样;
- 责任链模式 传播的消息是不变化的,即使变化也是在原来的消息上稍加修正,不会大幅改变结构;