相关概念
观察者模式:直接和公司签合同
订阅者模式:签大厂的外包公司
- 具体区别
- 观察者模式:
- 发布者直接触及到了订阅者
- 解决的是模块间的耦合问题(只是减少了耦合),可以使得两个分离的、毫不相关的模块也可以实现数据通信
- 发布订阅模式:
- 发布者不能直接触及到订阅者,而是由统一的第三方来完成实际的通信操作
- 发布者完全不用感知订阅者,不用关心怎么实现回调方法,事件的注册和触发都发生在独立于双方的第三方平台(事件总线)上,从而实现了完全的解耦
- 观察者模式:
什么是观察者模式
官方解释
定义对象间的一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式又被称为消息机制,它定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生变化时,所有依赖它的对象都得到通知并自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象与其他对象间的相互通知的问题。
「 是否符合设计原则 」
Subject和Observer相互分离,解耦Observer可以自由拓展Subject可自由拓展
观察者模式是由事件发生者在自身状态发生变化时发出通知,由观察者获取消息实现业务逻辑,事件发布者和接受者相互完全解耦,互不影响
在观察者模式中,需要两个主体,分别是目标对象Subject和观察者Observer
- 观察者需要实现update方法,用于供目标对象使用,该方法中可以执行自定义的业务的代码逻辑;
- 目标对象需要维护观察者数组数据,并且在自身发生变化时,通过调用自身方法
notify,通知到每个观察者执行对应的update方法;
// 定义观察者对象
class Observer {
/**
* @param 回调函数 收到目标对象通知时通过update进行执行
*/
constructor(fn){
if(typeof fn !== 'function') {
throw new Error('fn 必须是一个函数类型')
}else{
this.fn = fn,
this.name = fn.name;
}
}
// 目标对象触发更新 执行对应函数进行更新
update() {
this.fn(this.name)
}
}
// 定义目标对象
class Subject {
constructor(levelList = [1,2,3]) {
// 维护观察者列表数据
this.observerList = {
"normal" : []
},
this.levelList = ['normal',...levelList]
}
// 添加观察者到对应列表数据中
addObserver(observer,level="normal"){
if(!this.levelList.includes(level)) {
throw new Error(`订阅者 ${observer.name} 的级别错误,请重新设置`)
return
}
if(!this.observerList[level]) this.observerList[level] = []
this.observerList[level].push(observer)
}
// 删除订阅者
unloadObserver(observer,level = "normal"){
if(!this.levelList.includes(level)) {
throw new Error(`订阅者 ${observer.name} 的级别查询失败,请核实订阅者级别`)
return
}
if(!this.observerList[level].includes(observer)){
throw new Error(`订阅者 ${observer.name} 查询失败,请核实订阅者信息是否正确`)
return
}
this.observerList[level] = this.observerList[level].filter((item) => item !== observer)
}
// 通知所有观察者进行更新操作
notify() {
this.levelList = this.levelList.sort((a,b) => a-b)
if(this.levelList.indexOf("normal") === 0 && this.levelList.length > 1){
this.levelList.push(this.levelList.shift())
}
this.levelList.forEach((res) => {
this.observerList[res].forEach((observer) => {
observer.update()
})
})
}
}
const Lbxin1 = function(name){
// ......
console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin2 = function(name){
// ......
console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin3 = function(name){
// ......
console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin4 = function(name){
// ......
console.log(`成功通知到订阅者 ${name}`)
}
const normal = function(name){
// ......
console.log(`成功通知到订阅者 ${name}`)
}
const Lbxin1Ob = new Observer(Lbxin1)
const Lbxin2Ob = new Observer(Lbxin2)
const Lbxin3Ob = new Observer(Lbxin3)
const Lbxin4Ob = new Observer(Lbxin4)
const normalOb = new Observer(normal)
const subject = new Subject([1,3,2])
subject.addObserver(Lbxin1Ob,2)
subject.addObserver(Lbxin2Ob,3)
subject.addObserver(Lbxin3Ob,1)
subject.addObserver(Lbxin4Ob,2)
subject.addObserver(normalOb,1)
subject.addObserver(normalOb,2)
subject.addObserver(normalOb)
subject.addObserver(normalOb)
// subject.addObserver(Lbxin4Ob,4)
// sodu Error: 订阅者 Lbxin4 的级别错误,请重新设置
// subject.unloadObserver(normalOb,5)
// sodu Error: 订阅者 normal 的级别查询失败,请核实订阅者级别
// const normalOb11 = new Observer(normal)
// subject.unloadObserver(normalOb11)
// sodu Error: 订阅者 normal 查询失败,请核实订阅者信息是否正确
subject.unloadObserver(normalOb,1)
subject.notify()
// 成功通知到订阅者 Lbxin3
// 成功通知到订阅者 Lbxin1
// 成功通知到订阅者 Lbxin4
// 成功通知到订阅者 normal
// 成功通知到订阅者 Lbxin2
// 成功通知到订阅者 normal
// 成功通知到订阅者 normal
次例与下方的实现优点在于将观察者和目标对象分离,角色各自目的很明确,没有像发布订阅者模式中的调度中心作为中间者,实现了相关的抽离和拓展;
观察者模式的相关应用
postMessage 通信
利用window对象的
PostMessageAPI进行跨页面组件的通信,例如iframe间的相关通信,多进程(nodjs weborker)通讯,websocket通讯等
events模块的使用
node中的events是一个使用率很高的模块,其他的原生node模块都是基于它完成的,比如流、HTTP等;其底层还是用了观察者模式;
events模块的功能就是一个事件绑定,所有继承它的实例都具备事件处理的能力;
Vue框架的应用
生命周期、watch、组件更新过程,监听DOM树变化的
MutationObserver,各种异步回调包括:定时器、Promise.then、node的stream readline httpServer等;
适用场景
- 在一个抽象模型中,一个对象的行为依赖于另一个对象的状态,即一个对象的状态变化会影响另一个对象的行为数据
- 在系统中创建一个触发链,A影响B,B影响C......
数据的双向绑定
利用
Object.defineProperty()对数据进行劫持,设置一个监听器Observer,用来监听所有属性,如果属性发上变化了,就告诉订阅者Watcher去更新数据,最后指令解析器Compile解析对应的指令,进而会执行对应的更新函数,从而更新视图,实现了数据与视图的双向绑定
父子组件的通信
Vue中我们通过props完成父组件向子组件传递数据,子组件与父组件通信我们通过自定义事件即$on,$emit来实现,其实也就是通过$emit来发布消息,并对订阅者$on做统一处理;
在非父子组件通信中,可以用中央事件总线进行相关通信,此时也类似与发布订阅模式
DOM的事件监听
document.querySelector('#btn').addEventListener('click',function () {
console.log('按钮被点击了');
},false)
解决了一堆元素与相同数据同步的长期问题
拓展
- DOM方法 addEventListener() 和 removeEventListener()是用来分配和删除事件的函数。 这两个方法都需要三个参数,分别为:
- 事件名称(String)、要触发的事件处理函数(Function)、指定事件处理函数的时期或阶段(boolean)
- 当第三个参数设置为true就在
捕获过程中执行,反之就在冒泡过程中执行处理函数
- 什么是事件流
"DOM2事件流"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段; - document,documentElement和document.body三者之间的关系
document代表的是整个html页面document.documentElement代表是的<html>标签document.body代表的是<body>标签
- 事件穿透
- 当事件为冒泡时,从DOM树的最下面往上一层层执行事件(界面表现上为最顶层盒子事件先执行,以此类推);
- 当事件为捕获时,从DOM树的最上面一层层执行事件(界面表现为最底层盒子事件先执行,以此类推);
观察者模式优缺点
优点
- 可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并且降低观察目标和观察者之间的耦合度
- 支持简单广播通信和自动通知到所有已订阅过的对象
- 符合开放封闭要求(对拓展开放,对修改封闭)
- 观察目标可以对应多个观察者,这些观察者之间没有相互联系,可以通过增加和删除观察者,来使得系统更易于拓展;
- 观察目标和观察者之间的抽象耦合关系能够单独拓展和重用
缺点
- 通知的时间成本,当有多个观察者时
- 当观察目标和观察者之间存在循环引用时,有可能会出现系统崩溃的情况
- 观察目标和观察者之间是不透明的,无法知道两者的具体逻辑
具体案例分析
Vue中观察者模式的相关应用
- Event Bus和Event Emitter - 发布订阅者模式
- 数据双向绑定也是应用了观察者模式
- 主要有三个角色
- observer(监听器):常规的发布订阅者中的observer代表
订阅者,但在Vue双向绑定中observer不仅仅是数据监听器,还负责数据的转发(即也是一个发布者) - watcher(订阅者):Observer将数据转发给了
真正的订阅者 watcher对象,watcher接收到新的数据后会去更新视图 - compile(编译器):MVVM框架特有的角色,负责每个节点元素指令进行扫描和解析,指令的初始化、订阅者的创建等杂活都进行统一管理
- observer(监听器):常规的发布订阅者中的observer代表
- 主要有三个角色
发布订阅继承式实现「需求开发」
- 基类定义
// 定义发布者类
// 主要是添加、移除和通知的功能
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add invoked')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove invoked')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
// 定义订阅者类
// 被动方(被通知和后续'被迫'执行),主要是更新逻辑 具体触发机制是在发布者类中的「通知」模块中进行的
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update invoked')
}
}
- 基于基类定制化开发
// 发布方定制化 根据某一个状态来使得「订阅者监听某个特定状态的变化」
// 定义一个具体的需求文档(prd)发布类
class PrdPublisher extends Publisher {
constructor() {
super()
// 初始化需求文档
this.prdState = null
// 韩梅梅还没有拉群,开发群目前为空
// ? this.observers = []
console.log('PrdPublisher created')
}
// 该方法用于获取当前的prdState
getState() {
console.log('PrdPublisher.getState invoked')
return this.prdState
}
// 该方法用于改变prdState的值
setState(state) {
console.log('PrdPublisher.setState invoked')
// prd的值发生改变
this.prdState = state
// 需求文档变更,立刻通知所有开发者
this.notify()
}
}
// 订阅方定制化 使得开发者任务变得具体起来
class DeveloperObserver extends Observer {
constructor() {
super()
// 需求文档一开始还不存在,prd初始为空对象
this.prdState = {}
console.log('DeveloperObserver created')
}
// 重写一个具体的update方法
update(publisher) {
console.log('DeveloperObserver.update invoked')
// 更新需求文档
this.prdState = publisher.getState()
// 调用工作函数
this.work()
}
// work方法,一个专门搬砖的方法
work() {
// 获取需求文档
const prd = this.prdState
// 开始基于需求文档提供的信息搬砖。。。
...
console.log('996 begins...')
}
}
通过new一个
PrdPublisher对象来通过setState来通知订阅者进行触发执行指定的逻辑,从而实现目标对象的状态变化时,会通知所有观察者对象,使得它们可以自动更新
// 创建订阅者:前端开发李雷
const liLei = new DeveloperObserver()
// 创建订阅者:服务端开发小A
const A = new DeveloperObserver()
// 创建订阅者:测试同学小B
const B = new DeveloperObserver()
// 韩梅梅出现了
const hanMeiMei = new PrdPublisher()
// 需求文档出现了
const prd = {
// 具体的需求内容
...
}
// 韩梅梅开始拉群
hanMeiMei.add(liLei)
hanMeiMei.add(A)
hanMeiMei.add(B)
// 韩梅梅发送了需求文档,并@了所有人
hanMeiMei.setState(prd)
订阅报刊
在创建报刊的过程中会有很多的额外的操作,并且在业务发展过程中,会不断的添加或修改这些额外的逻辑,所以适合用观察者模式进行设计;
发布者Magazine需要有一个subscribers属性,是一个数组类型,后续的订阅行为都通过指定方法存放到这个数组中,当有新报刊时需要通知操作,该操作即是调用订阅者存放的这些方法;
观察者模式需要有的API包括
- subscribers属性用于存放订阅者的行为逻辑 subscribers
- 添加订阅,保存订阅逻辑到subscribers中 subscribe()
- 移除订阅的API,用于将订阅逻辑从subscribers中删除 unsubscribe()
- 通知订阅者的API,用于调用订阅者的订阅方法 publish()
- 区分订阅类型的变量值,用于区分订阅的报刊类型 type
const Magazine = {
// 存放订阅者方法
subscribers: {
'any': []
},
// 订阅
subscribe: function(type = 'any',fn){
if(!this.subscribers[type]){
this.subscribers[type] = []
}
this.subscribers[type].push(fn)
},
//退订
unsubscribe:function(type = 'any', fn){
this.subscribers[type] = this.subscribers[type].filter((item)=>item !== fn)
},
// 发布更新的消息
publish(type='any',...args){
// 根据不同类型进行指定类型的订阅者通知
this.subscribers[type].forEach((item) => {
item(...args)
})
}
}
function subscribeBack(info){
console.log("订阅的报刊更新啦:",info)
}
const subscribeList = [
{
name: 'Lbxin01',
type: 'front-end',
address: '上地'
},
{
name: 'Lbxin02',
type: 'rear-end',
address: '十里堡'
}
]
subscribeList.forEach(({name,type,address}) => {
Magazine.subscribe(type,subscribeBack)
})
Magazine.publish('front-end','vue3.0 正式发布')
Magazine.publish('rear-end','deno 更新中')
Magazine.unsubscribe('rear-end',subscribeBack)
Magazine.publish('front-end','TS 8.0正式发布')
Magazine.publish('rear-end','node 升级迭代上线了')
// 订阅的报刊更新啦: vue3.0 正式发布
// 订阅的报刊更新啦: deno 更新中
// 订阅的报刊更新啦: TS 8.0正式发布