从 Vue2 的响应式原理浅谈【观察者模式】和【发布订阅】

2,844 阅读9分钟

Vue 的响应式原理想必我们都有所了解,大致分成了以下两步:

  1. 通过 Object.defineProperty 劫持数据,收集依赖

  2. 当数据被访问或更新时,通知对应依赖去响应视图的变化

先来看下 Object.defineProperty 是怎么劫持数据的:

function proxyData(data, key, defaultValue) {
    let value = defaultValue
    Object.defineProperty(data, key, {
        enumerable: true,
        get() {
            console.log('获取属性')
            return value
        },
        set(newVal) {
            value = newVal
            console.log('更新属性')
        }
    })
}

假设有个对象 data,我们在上面添加一个新属性 num, 值为 200,再将其值更新为 300

var data = {}
proxyData(data, 'num', 200)

console.log(data.num) // 获取属性

data.num = 300 // 更新属性

可以发现,在获取更新属性的时候会触发对应的 getset 方法,也就是 gettersetter

所以我们可以通过 getter 来收集视图中的依赖,在 setter 的时候更新视图

也就是说 gettersetter 的内部操作实际上就是运用了 观察者模式 来实现的

而与之对应的还有个 发布订阅模式,接下来我们会通过一些例子来说明这两个模式的区别和联系

💭本文首发掘金: 从Vue的响应式原理浅谈【观察者模式】和【发布订阅】

介绍

在开始之前先来熟悉下这两个模式:

发布订阅模式: 订阅者向事件调度中心(PubSub)注册(subscribe)监听,当事件调度中心(PubSub)发布通知时(publish),订阅者的监听事件将会被触发。

观察者模式: 定义了对象之间 一对多 的依赖关系,它只有两个角色,分别是观察的目标对象 Subject 和观察者对象 Observer,当一个 目标对象 的状态发生改变时,所有依赖于它的 观察者对象 都会收到通知。

显然,两者唯一不同的地方就是 发布订阅模式观察者模式 多了个 事件调度中心 机制

这两张图解释了两个模式在操作流程上的一些不同:

  • 发布订阅模式

发布订阅模式

  • 观察者模式

观察者模式

发布订阅模式

我们先来热身下,写一个简单的 发布订阅模式 玩玩

我们来举个栗子说明下它的应用,小x用微信点餐肯德基

小x用微信点了肯德基,取了个号码 200,接着他不想一直等着,于是去周边转了转

当小x的餐做好了的时候,微信推送消息 200 号到了,通知小x去取餐

抽象出发布订阅模式:

  1. 小x要点餐,那么小x就是消息的订阅者,即 subscribe
  2. 点餐的渠道是微信,也就是 事件调度中心,即 PubSub
  3. 肯德基通过微信推送消息给小x,告诉他的餐做好了,是消息的发布者,即 publish

发布者(肯德基)借用事件调度中心(微信,即 PubSub)提供的 publish 方法通知小x的餐做好了

订阅者(小x)则通过 subscribe 接收取餐的通知

事件调度中心

将刚才的场景抽象为代码:

// 事件调度中心
function PubSub() {
  // 订阅者集合
  // 集合的每个对象里都包含了订阅者类型 type,及要做的事情 callback
  this.subs = {}
}

// 订阅者订阅
PubSub.prototype.subscribe = function (type, callback) {
  // 每增加一个订阅者,就将其信息添加到订阅者集合中
  this.subs[type] = callback
}

// 发布者发布
PubSub.prototype.publish = function (type, message) {
  const callback = this.subs[type]
  callback(`${type}${message}`)
}

示例代码

我们假设订阅者有小明和小红两个人,他们都点餐了,对应的代码如下:

// 创建一个事件调度中心(微信)
var ps = new PubSub()

// 小明在微信点餐了
ps.subscribe('小明,200号', function(msg){
  console.log('微信通知:', msg)
})

// 小红在微信点餐了
ps.subscribe('小红,201号', function(msg){
  console.log('微信通知:', msg)
})

// 肯德基做好了小明的食物
ps.publish('小明,200号', '您的餐好了')

// 肯德基做好了小红的食物
ps.publish('小红,201号', '您的餐好了')

可见,小明和小红都先在微信点了餐排了号,完成订阅的过程;

接着微信再分别通知小明和小红的餐做好了,即完成了发布的过程。

场景

类似的过程在生活中也有体现

比如: 定外卖、订牛奶、订报纸、网购、公众号订阅消息等等,这些都可以用发布订阅模式来设计。

从现有的开发过程中也可以看到一些影子:

Vue 总线 Event Bus 中事件的注册和分发

DOM 事件中的事件监听和回调

特点

  • 订阅者和发布者之间本身是没有关系的,唯一把他们关联到一块的就是 事件调度中心 提供的发布和订阅事件,就好像是两个完全不相干的人一样,他们只是借助了这个事件调度中心 去完成一些事情,事件调度中心 在其中就相当于中介的影子
  • 低耦合,适用于总线模块(如 EventBus

观察者模式

观察者模式:定义了对象之间 一对多 的依赖关系,它只有两个角色,分别是观察的目标对象 Subject 和观察者对象 Observer

当目标对象 Subject 的状态发生改变时,所有依赖它的观察者对象 Observer 都会得到响应

我们还借用刚才的“小x点餐” ,只不过这次不用“微信”点餐了,也不在周围瞎转悠了,而是直接在店里排队叫号

我们分析下所含的角色:

  1. 小x点餐等待排队叫号,那么小x就是观察者对象,即 Observer
  2. 肯德基每叫一次号,小x就会看看是否轮到自己了,所以肯德基是小x观察的目标对象,即 Subject

Observer(小x)直接在 Subject(肯德基)取号排队

Subject(肯德基)通知当前排到多少号时,Observer 会触发它自己的 update(查看号码是否轮到自己)

观察者对象

function Observer(type, callback) {
    this.type = type
    this.callback = callback
}

// 观察者收到通知后去做的回调
Observer.prototype.update = function (type) {
    return this.callback(type)
}

观察的目标对象

function Subject() {
    // 观察者集合,是个对象数组
    // 每一项存放的都是一个观察者
    this.observers = []
}

// 将观察者加入到队列中(赋予观察者排队号码)
Subject.prototype.addObserver = function (observer) {
    this.observers.push(observer)
}

// 通知所有观察者事件(通知观察者们当前排到多少号)
Subject.prototype.notify = function (type) {
    this.observers.forEach((observer, index) => {
        observer.update(type)
    })
}

为方便起见,我们将 Observer 的回调抽成一个函数:

function callback(currentType) {
  if (this.type !== currentType) {
    console.log(`${this.type}想:还没排到我`)
  } else {
    console.log(`${this.type}想:排到我了,我该去取餐了`)
  }
}

示例代码

我们来试试看

var ob1 = new Observer('小明,100号', callback)
var ob2 = new Observer('小红,101号', callback)
var ob3 = new Observer('小花,102号', callback)

// 肯德基
var subject = new Subject()

// 将三个观察者分别加入到排队队列中
subject.addObserver(ob1)
subject.addObserver(ob2)
subject.addObserver(ob3)

// 通知当前号码
subject.notify('小明,100号')

// 通知当前号码
subject.notify('小花,102号')

// 通知当前号码
subject.notify('小红,101号')

执行完后你可能会也发现: 已经取到餐的观察者依然会被叫号

代码优化

优化下 callback,添加返回值:

function callback(currentType) {
  if (this.type !== currentType) {
    console.log(`${this.type}想:还没排到我`)
    return false
  }
  
  console.log(`${this.type}想:排到我了,我该去取餐了`)
  return true
}

通知代码 notify 也优化下:

// 通知所有观察者事件(通知观察者们当前排到多少号)
Subject.prototype.notify = function (type) {
  // 记录哪个观察者拿到餐了
  let who;

  this.observers.forEach((observer, index) => {
      // 这里判断下是否更新完毕(当前观察者是否拿到餐了)
      const isUpdate = observer.update(type)
      // 若更新完毕,则记录这个观察者
      isUpdate && (who = index)
  })

  // 若有观察者拿到餐了,则将它从观察者集合中移除
  who !== undefined && this.observers.splice(who, 1)
}

最终代码

将刚才优化的整理下,可以自行在控制台测验:

function Observer(type, callback) {
    this.type = type
    this.callback = callback
}
// 观察者收到通知后去做的回调
Observer.prototype.update = function (type) {
    return this.callback(type)
}

function Subject() {
    // 观察者集合,是个对象数组
    // 每一项存放的都是一个观察者
    this.observers = []
}

// 将观察者加入到队列中(赋予观察者排队号码)
Subject.prototype.addObserver = function (observer) {
    this.observers.push(observer)
}

// 通知所有观察者事件(通知观察者们当前排到多少号)
Subject.prototype.notify = function (type) {
  // 记录哪个观察者拿到餐了
  let who;

  this.observers.forEach((observer, index) => {
      // 这里判断下是否更新完毕(当前观察者是否拿到餐了)
      const isUpdate = observer.update(type)
      // 若更新完毕,则记录这个观察者
      isUpdate && (who = index)
  })

  // 若有观察者拿到餐了,则将它从观察者集合中移除
  who !== undefined && this.observers.splice(who, 1)
}


function callback(currentType) {
  if (this.type !== currentType) {
    console.log(`${this.type}想:还没排到我`)
    return false
  }
  
  console.log(`${this.type}想:排到我了,我去取餐了`)
  return true
}


var ob1 = new Observer('小明,100号', callback)
var ob2 = new Observer('小红,101号', callback)
var ob3 = new Observer('小花,102号', callback)

// 肯德基
var subject = new Subject()

// 将三个观察者分别加入到排队队列中
subject.addObserver(ob1)
subject.addObserver(ob2)
subject.addObserver(ob3)

// 通知第一个号码
subject.notify('小明,100号')

// 通知第二个号码, 请自行打开注释依次执行
// subject.notify('小花,102号')

// 通知第三个号码,请自行打开注释依次执行
// subject.notify('小红,101号')

场景

  • 过年回家时可能会用到第三方抢票软件,那些软件会不停地检测是否有票,一旦检测到有剩余就立即锁定帮你购买。

抢票软件: Observer

车票: Subject

响应的结果: 锁定并购票

  • 再比如经典的气象观测案例,气象局会追踪气压,温/湿度,卫星云图等参数,当这些参数变化时会将计算好的数据实时更新到各个网站

气象局: Observer

气象参数: Subject

响应的结果: 更新数据到各个网站

  • Vue 的响应式依赖收集
  • React 对应的状态管理库 MobX

特点

  • 没有事件调度中心作为中介,只有两个角色,目标对象 SubjectObserver 都要去实现各自的方法(notifyupdate
  • 由于两个角色直接联系,所以耦合性会更强,且目标对象 Subject 更为主动,会自行对依赖进行收集和管理,所以这对那些依赖收集的场景更为适用

总结

在发布订阅模式中,发布者和订阅者不会直接联系,而是直接通过事件调度中心去通信

而观察者模式与发布订阅模式恰恰相反,区别在于,没有事件调度中心。观察者和观察的对象之间相互依赖,耦合在一起

你可以把发布订阅模式理解成解耦了的观察者模式(即观察者模式就是高度耦合的发布订阅模式)

观察者模式适用于内部元素关联性强、职责较单一的模块,而发布订阅更适合去做跨模块的通信

以上,如有不足,欢迎评论区指正 ~

参考资料

发布订阅模式-云社区

观察者模式VS订阅发布模式

Head First从气象观测分析——观察者模式