我们前不久写过发布订阅模式:
// 发布者 Publisher class Pub { constructor() { this.deps = []; } addDep(dep) { this.deps.push(dep); } publish(dep) { this.deps.forEach(item => item === dep && item.notify()); } } // 订阅者 Subscriber class Sub { constructor(val) { this.val = val; } update(callback) { callback(this.val) } } // 调度中心 class Dep { constructor(callback) { // 核心是这个 callback 函数; this.subs = []; this.callback = callback; } addSub(sub) { this.subs.push(sub); } notify() { this.subs.forEach(item => item.update(this.callback)); } } let pub = new Pub() // 实例化一个发布者 // 实例化一个调度中心,传入一个用于处理数据的函数; const dep1 = new Dep((data) => console.log('我是调度中心,我先把消息处理一下,然后发给 ===》》》', data)) let sub1 = new Sub("订阅者1") // 实例化订阅者1 let sub2 = new Sub("订阅者2") // 实例化订阅者2 pub.addDep(dep) // 发布者绑定调度中心 dep.addSub(sub1) // 调度中心添加订阅者1 dep.addSub(sub2) // 调度中心添加订阅者2 pub.publish(dep) // 发布者把消息推给调度者 // 我是调度中心,我先把消息处理下先 订阅者1 // 我是调度中心,我先把消息处理下先 订阅者2
但是这样看,似乎有点太复杂了:
- 发布者需要有两个方法,绑定调度者 Dep,把消息推知给调度者;
- 调度者也有两个方法,绑定订阅者 Sub,把消息推送给订阅者;
- 订阅者有一个方法,执行函数;
这里面最重要的是有一个回调函数,作为调度中心的入参,会传给 Sub 执行;
于是,本篇带来 简化 了的思路进行理解:
比方说天气预报这个场景:气象站是需要发布信息的;建筑工地、船舶行业、普通游客是需要这些信息的;
如果我们直接强绑定这个通知关系,即:
function weatherWarning(weatherStatus){ if(weatherStatus==='warning'){ // 糟糕的天气 buildingsite.stopwork() // 工地停工 ships.mooring() // 船舶停航 tourists.canceltrip() // 旅游取消 } } weatherWarning("warning") // 发布坏天气通知
这样做,有无毛病?
还得是它俩:有毛病!违背开闭原则、违背单一职责原则;
违背开闭原则:上例中,如果有新的群体需要获取天气信息,要不断修改 weatherWarning 函数;
违背单一职责原则:上例中,任何一个群体代码执行错误,都会影响 weatherWarning 函数体代码的向下执行;
所以,还得改,于是引入:调度中心 Dep,这里叫 EventEmit
由调度中心来绑定需要信息的群体,即绑定订阅器,然后由调度中心发布信息给订阅者;
const EventEmit = function() { // 调度中心 this.events = {}; this.on = function(name, cb) { // 绑定订阅器 if (this.events[name]) { this.events[name].push(cb); // 支持同一个订阅器执行多个事情 } else { this.events[name] = [cb]; } }; this.trigger = function(name, ...arg) { // 发送消息 if (this.events[name]) { this.events[name].forEach(eventListener => { eventListener(...arg); }); } }; };
let weatherEvent = new EventEmit() // 实例化一个调度中心 weatherEvent.on('warning', function () { // 绑定发布通知的关系 // buildingsite.stopwork() console.log('buildingsite.stopwork()') }) weatherEvent.on('warning', function () { // 绑定发布通知的关系 // ships.mooring() console.log('ships.mooring()') }) weatherEvent.on('warning', function () { // 绑定发布通知的关系 // tourists.canceltrip() console.log('tourists.canceltrip()') }) weatherEvent.trigger('warning') // 发布消息
当项目中存在一对多的依赖,且每个模块相对独立,可以考虑使用发布订阅模式来重构代码,即由调度中心来绑定、通知。
有工友可能疑问:这个怎么和之前说的【观察者模式】长得那么像?
class Subject{// 被观察者 constructor(){ this.observers=[] } add(observer){ this.observers.push(observer) } notify(weatherStatus){ this.observers.forEach(i=>i(weatherStatus)) } } let sub = new Subject() sub.add((reason)=>{ // buildingsite.stopwork() console.log('工地停工,因为天气:',reason) }) sub.add((reason)=>{ // ships.stopwork() console.log('船舶停航,因为天气:',reason) }) sub.add((reason)=>{ // tourists.canceltrip() console.log('旅游取消,因为天气:',reason) }) sub.notify("warning") // sub 发布消息 // 工地停工,因为天气: warning // 船舶停航,因为天气: warning // 旅游取消,因为天气: warning
没错,我们可以再简化理解:观察者模式是发布订阅模式的一部分,如果你把被观察者视作调度中心的话呢,这就是发布订阅模式,如果你把订阅中心视作被观察者,那就是观察者模式;两者是可以互相转化的。
观察者模式:A 推给 ob1、ob2、ob3,一对多;
发布订阅模式: A 推给 Dep ,Dep 再推给 ob1、ob2、ob3,一对一,再对多;
发布订阅模式应该是我们前端开发者最常用的设计模式:
element.addEventListener('click', function(){ //... })