前端有两种常用的通信模式:观察者和发布 / 订阅模式。两者最主要的区别是一对多单向通信还是多对多双向通信的问题。
以微前端为例,如果只需要主应用向各个子应用单向广播通信,并且多个子应用之间互相不需要通信,那么只需要使用观察者模式即可,而如果主应用需要和子应用双向通信,或者子应用之间需要实现去中心化的双向通信,那么需要使用发布 / 订阅模式。
在浏览器中会使用观察者模式来实现内置 API 的单向通信,例如 IntersectionObserver、MutationObserver、ResizeObserver 以及 PerformanceObserver 等,而发布 / 订阅模式则通常是框架提供的一种供外部开发者自定义通信的能力,例如浏览器中的 EventTarget、Node.js 中的 EventEmitter、Vue.js 中的 $emit 等。
观察者模式
观察者模式需要包含 Subject 和 Observer 两个概念,其中 Subject 是需要被观察的目标对象,一旦状态发生变化,可以通过广播的方式通知所有订阅变化的 Observer,而 Observer 则是通过向 Subject 进行消息订阅从而实现接收 Subject 的变化通知,具体如下所示:
我们以浏览器的 MutationObserver 为例,来看下观察者模式如何运作:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="subject"></div>
</body>
<script>
# 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.')
} else if (mutation.type === 'attributes') {
console.log(
'The ' + mutation.attributeName + ' attribute was modified.'
)
}
}
}
# 创建第一个 Observer
const observer1 = new MutationObserver(callback)
# Subject 目标对象
const subject = document.getElementById('subject')
# Observer 的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true }
# Observer 订阅 Subject 的变化
observer1.observe(subject, config)
# 创建第二个 Observer
const observer2 = new MutationObserver(callback)
# Observer 订阅 Subject 的变化
observer2.observe(subject, config)
# Subject 的属性变化,会触发 Observer 的 callback 监听
subject.className = 'change class'
# Subject 的子节点变化,会触发 Observer 的 callback 监听
subject.appendChild(document.createElement('span'))
# 这里为什么需要 setTimeout 呢?如果去除会有什么影响吗?
setTimeout(() => {
// 取消订阅
observer1.disconnect()
observer2.disconnect()
})
</script>
</html>
当 DOM 元素(Subject 目标对象)改变自身的属性或者添加子元素时,都会将自身的状态变化单向通知给所有订阅该变化的观察者。
当然上述 Web API 内部包装了很多功能,例如观察者配置。我们可以设计一个更加便于理解的观察者通信方式:
class Subject {
constructor() {
this.observers = [];
}
// 添加订阅
subscribe(observer) {
this.observers.push(observer);
}
// 取消订阅
unsubscribe() {}
// 广播信息
broadcast() {
this.observers.forEach((observer) => observer.update());
}
}
class Observer {
constructor() {}
// 实现一个 update 的接口,供 subject 耦合调用
update() {
console.log("observer update...");
}
}
const subject = new Subject();
subject.subscribe(new Observer());
subject.broadcast();
subject.subscribe(new Observer());
subject.broadcast();
上述观察者模式没有一个实体的 Subject 对象,我们可以结合 DOM 做一些小小的改动,例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 目标对象 -->
<input type="checkbox" id="checkbox" />
<!-- 观察者 -->
<div id="div"></div>
<h1 id="h1"></h1>
<span id="span"></span>
<script>
class Subject {
constructor() {
this.observers = [];
}
// 添加订阅
subscribe(observer) {
this.observers.push(observer);
}
// 取消订阅
unsubscribe() {}
// 广播信息
broadcast(value) {
this.observers.forEach((observer) => observer.update(value));
}
}
# 观察的目标对象
const checkbox = document.getElementById("checkbox");
# 将 subject 实例挂载到 DOM 对象上(也可以单独使用)
checkbox.subject = new Subject();
checkbox.onclick = function (event) {
# 通知观察者 checkbox 的变化
checkbox.subject.broadcast(event.target.checked);
};
# 观察者
const span = document.getElementById("span");
const div = document.getElementById("div");
const h1 = document.getElementById("h1");
# 观察者实现各自 update 接口
span.update = function (value) {
span.innerHTML = value;
};
div.update = function (value) {
div.innerHTML = value;
};
h1.update = function (value) {
h1.innerHTML = value;
};
# 添加订阅
checkbox.subject.subscribe(span);
checkbox.subject.subscribe(div);
checkbox.subject.subscribe(h1);
</script>
</body>
</html>
发布 / 订阅模式
发布 / 订阅模式需要包含 Publisher、Channels 和 Subscriber 三个概念,其中 Publisher 是信息的发送者,Subscriber 是信息的订阅者,而 Channels 是信息传输的通道,如下所示:
发布者可以向某个通道传输信息,而订阅者则可以订阅该通道的信息变化。
通过新增通道,可以将发布者和订阅者解耦出来,从而形成一种去中心化的通信模式。
如上图所示,订阅者本身也可以是发布者,从而实现事件的双向通信。
我们以浏览器的 EventTarget 为例,来看下发布 / 订阅模式如何运作:
const event = new EventTarget();
// event 是订阅者
event.addEventListener("channel1", (e) => console.log(e.detail));
// event 是发布者
event.dispatchEvent(
new CustomEvent("channel1", { detail: { hello: true } })
);
需要注意的是先订阅,后发布,如果先发布后订阅则不行:
event.dispatchEvent(
new CustomEvent("channel2", { detail: { hello: true } })
);
// 由于先发布后订阅,导致订阅失败,但是发布者不感知订阅者的失败状态
event.addEventListener("channel2", (e) => console.log(e.detail));
我们可以通过简单的几行代码实现上述功能,如下所示:
class Event {
constructor() {
this.channels = {};
// 这里的 token 也可以是随机生成的 uuid
this.token = 0;
}
// 实现订阅
subscribe(channel, callback) {
if (!this.channels[channel]) this.channels[channel] = [];
this.channels[channel].push({
channel,
token: ++this.token,
callback,
});
return this.token;
}
// 实现发布
publish(channel, data) {
const subscribers = this.channels[channel];
if (!subscribers) return;
let len = subscribers.length;
while (len--) {
subscribers[len]?.callback(data, subscribers[len].token);
}
}
// 取消订阅
unsubscribe(token) {
for (let channel in this.channels) {
const index = this.channels[channel].findIndex(
(subscriber) => subscriber.token === token
);
if (index !== -1) {
this.channels[channel].splice(index, 1);
if (!this.channels[channel].length) {
delete this.channels[channel];
}
return token;
}
}
}
}
const event = new Event();
const token = event.subscribe("channel1", (data) => console.log('token: ', data));
const token1 = event.subscribe("channel1", (data) => console.log('token1: ', data));
// 打印 token 和 token1
event.publish("channel1", { hello: true });
event.unsubscribe(token);
// 打印 token1,因为 token 取消了订阅
event.publish("channel1", { hello: true });
发布 / 订阅模式和观察者模式存在明显差异:
-
首先在功能上观察者模式是一对多的单向通信模式,而发布 / 订阅模式是多对多的双向通信模式。
-
其次观察者模式需要一个中心化的 Subject 广播消息,并且需要感知 Observer(例如上述的
observers列表) 实现通知,是一种紧耦合的通信方式。而发布 / 订阅模式中的发布者只需要向特定的通道发送信息,并不感知订阅者的订阅状态,是一种松散解耦的通信方式。