《JavaScript 设计模式与开发实战》—— 发布订阅模式

960 阅读3分钟

发布订阅模式

发布——订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。

现实中得发布订阅

中秋节快到了,前端超市的月饼被各个前端大佬清空了,导致小李没能买到送女朋友的月饼,于是小李加了超市售货员的微信,每隔一分钟发微信询问月饼是否到货(轮询)。

但是除了小李还有小明、小花等等,他们都想买月饼,也都每隔一分钟发送微信询问,于是售货员小姐姐不干了。

新来的售货员小姐姐为了避免这种情况,记下了所有人的电话(订阅)并告诉所有想要买月饼的人,月饼到货了我会主动通知(发布)。

发布——订阅模式的作用

在上面的例子中,使用发布订阅模式有着显而易见的优点:

  • 避免了轮询,售货员会在合适的时间通知订阅者
  • 售货员和订阅者之间解耦,通过电话本建立联系,只要电话本存在,无所谓售货员是否离职。

第一点说明发布——订阅模式可以用于异步编程,替代传递回调函数的方案。

第二点说明发布——订阅模式可以取代对象之间的硬编码通知机制,一个对象不用在显示调用另外一个对象的某个接口。

发布——订阅事件

事实上,在 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 的方式自动创建。

小结

发布——订阅优点

  • 时间上的解耦
  • 对象之间的解耦

缺点

  • 创建订阅者本身要消耗一定时间的内存,订阅一个消息后,这个消息并不一定会发生,但订阅者会一直存在
  • 发布——订阅模式会弱化对象之间的联系,但是过度使用,对象和对象间的必要联系也会被隐藏,会导致程序难以跟踪维护和理解。