发布订阅模式
发布——订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。
现实中得发布订阅
中秋节快到了,前端超市的月饼被各个前端大佬清空了,导致小李没能买到送女朋友的月饼,于是小李加了超市售货员的微信,每隔一分钟发微信询问月饼是否到货(轮询)。
但是除了小李还有小明、小花等等,他们都想买月饼,也都每隔一分钟发送微信询问,于是售货员小姐姐不干了。
新来的售货员小姐姐为了避免这种情况,记下了所有人的电话(订阅)并告诉所有想要买月饼的人,月饼到货了我会主动通知(发布)。
发布——订阅模式的作用
在上面的例子中,使用发布订阅模式有着显而易见的优点:
- 避免了轮询,售货员会在合适的时间通知订阅者
- 售货员和订阅者之间解耦,通过电话本建立联系,只要电话本存在,无所谓售货员是否离职。
第一点说明发布——订阅模式可以用于异步编程,替代传递回调函数的方案。
第二点说明发布——订阅模式可以取代对象之间的硬编码通知机制,一个对象不用在显示调用另外一个对象的某个接口。
发布——订阅事件
事实上,在 DOM 上绑定事件函数,就是一个发布——订阅事件。
document.body.addEventListener('click', () => {
console.log(1);
});
document.body.addEventListener('click', () => {
console.log(2);
});
document.body.addEventListener('click', () => {
console.log(3);
});
addEventListener 其实是一个添加订阅的过程。当用户点击时,通知所有订阅者。
我们可以实现一个自己的 addEventListener。
let clientList = {};
function myAddEventListener(key, fn) {
if (clientList[key]) {
clientList[key].push(fn);
} else {
clientList[key] = [fn];
}
}
function myEventTrigger(key) {
if (!clientList[key]) {
// key 不存在
return;
}
clientList[key].forEach((fn) => {
fn();
});
}
// 测试
myAddEventListener('click', () => {
console.log(1);
});
myAddEventListener('click', () => {
console.log(2);
});
myEventTrigger('click'); // 1, 2
我们实现了一个自己得事件监听发布——订阅函数,但是这个函数现在还不能取消订阅。
let clientList = {};
function myAddEventListener(key, fn) {
if (clientList[key]) {
clientList[key].push(fn);
} else {
clientList[key] = [fn];
}
return function unsubscribe() {
const index = clientList[key].indexOf(fn);
clientList[key].splice(index, 1);
};
}
function myEventTrigger(key) {
if (!clientList[key]) {
// key 不存在
return;
}
clientList[key].forEach((fn) => {
fn();
});
}
// 测试
const unsubscribe1 = myAddEventListener('click', () => {
console.log(1);
});
const unsubscribe2 = myAddEventListener('click', () => {
console.log(2);
});
const unsubscribe3 = myAddEventListener('click', () => {
console.log(3);
});
unsubscribe2();
myEventTrigger('click'); // 1, 2
我们通过订阅时返回取消订阅函数来实现取消订阅。现在这样写用起来比较不方便有兴趣得童鞋可以封装一下,这里就不展开了。
Vue3 中的发布订阅
Vue3 响应式是基于发布——订阅模式。
例子中的 clientList 在 Vue3 源码中就是 targetMap[target],key 对应 targetMap[target][key], targetMap[target][key] 是一个 Set,里面存储了多有订阅者,在 Vue3 中对应 effect 副作用函数。
// const targetMap = new WeakMap<any, KeyToDepMap>()
// 数据结构
{
targetMap: {
target: {
key: [effect, effect]; // Set
}
}
}
track 函数收集订阅,trigger 函数触发订阅通知。effect 函数为订阅者和被订阅的对象间建立联系。
const obj = { foo: 1 };
effect(() => {
console.log(obj.foo);
track(obj, TrackOpTypes.GET, 'foo');
});
obj.foo = 2;
trigger(obj, TriggerOpTypes.SET, 'foo');
我们可以手动创建响应式,在 Vue 中 Vue 会通过拦截 get 和 set 的方式自动创建。
小结
发布——订阅优点
- 时间上的解耦
- 对象之间的解耦
缺点
- 创建订阅者本身要消耗一定时间的内存,订阅一个消息后,这个消息并不一定会发生,但订阅者会一直存在
- 发布——订阅模式会弱化对象之间的联系,但是过度使用,对象和对象间的必要联系也会被隐藏,会导致程序难以跟踪维护和理解。