Vue 的响应式原理想必我们都有所了解,大致分成了以下两步:
-
通过 Object.defineProperty 劫持数据,收集依赖
-
当数据被访问或更新时,通知对应依赖去响应视图的变化
先来看下 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 // 更新属性
可以发现,在获取和更新属性的时候会触发对应的 get 和 set 方法,也就是 getter 和 setter
所以我们可以通过 getter 来收集视图中的依赖,在 setter 的时候更新视图
也就是说 getter 和 setter 的内部操作实际上就是运用了 观察者模式 来实现的
而与之对应的还有个 发布订阅模式,接下来我们会通过一些例子来说明这两个模式的区别和联系
💭本文首发掘金: 从Vue的响应式原理浅谈【观察者模式】和【发布订阅】
介绍
在开始之前先来熟悉下这两个模式:
发布订阅模式: 订阅者向事件调度中心(
PubSub)注册(subscribe)监听,当事件调度中心(PubSub)发布通知时(publish),订阅者的监听事件将会被触发。观察者模式: 定义了对象之间
一对多的依赖关系,它只有两个角色,分别是观察的目标对象Subject和观察者对象Observer,当一个目标对象的状态发生改变时,所有依赖于它的观察者对象都会收到通知。
显然,两者唯一不同的地方就是 发布订阅模式 比 观察者模式 多了个 事件调度中心 机制
这两张图解释了两个模式在操作流程上的一些不同:
- 发布订阅模式
- 观察者模式
发布订阅模式
我们先来热身下,写一个简单的 发布订阅模式 玩玩
我们来举个栗子说明下它的应用,小x用微信点餐肯德基
小x用微信点了肯德基,取了个号码 200,接着他不想一直等着,于是去周边转了转
当小x的餐做好了的时候,微信推送消息 200 号到了,通知小x去取餐
抽象出发布订阅模式:
- 小x要点餐,那么小x就是消息的订阅者,即
subscribe - 点餐的渠道是微信,也就是
事件调度中心,即PubSub - 肯德基通过微信推送消息给小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点餐” ,只不过这次不用“微信”点餐了,也不在周围瞎转悠了,而是直接在店里排队叫号
我们分析下所含的角色:
- 小x点餐等待排队叫号,那么小x就是观察者对象,即
Observer - 肯德基每叫一次号,小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
特点
- 没有事件调度中心作为中介,只有两个角色,目标对象
Subject和Observer都要去实现各自的方法(notify和update) - 由于两个角色直接联系,所以耦合性会更强,且目标对象
Subject更为主动,会自行对依赖进行收集和管理,所以这对那些依赖收集的场景更为适用
总结
在发布订阅模式中,发布者和订阅者不会直接联系,而是直接通过事件调度中心去通信
而观察者模式与发布订阅模式恰恰相反,区别在于,没有事件调度中心。观察者和观察的对象之间相互依赖,耦合在一起
你可以把发布订阅模式理解成解耦了的观察者模式(即观察者模式就是高度耦合的发布订阅模式)
观察者模式适用于内部元素关联性强、职责较单一的模块,而发布订阅更适合去做跨模块的通信
以上,如有不足,欢迎评论区指正 ~
参考资料