一、初识发布订阅:房产中介的营销套路
想象这样一个场景:康总(一个钱多但不太聪明的土豪)冲进房产中介,把一沓钞票拍在桌上:"老子要买房!"虽然现在没房源,但精明的中介怎么会放过这种狗大户?他们立即把康总拉进了"VIP购房群",等有新房源时就在群里通知。这就是发布订阅模式的现实版:
- 订阅 :康总加群(
on
方法) - 发布 :中介发房源通知(
emit
方法) - 事件中心 :那个微信群(
eventList
对象)
javascript
class EventEmitter {
constructor() {
this.eventList = {}; // 中介的客户登记簿
}
// 客户登记(订阅)
on(eventName, cb) {
if (!this.eventList[eventName]) {
this.eventList[eventName] = []; // 新建一个客户群
}
this.eventList[eventName].push(cb); // 把客户加进群
}
// 发布通知(emit)
emit(eventName) {
const handlers = this.eventList[eventName]?.slice(); // 安全拷贝
handlers?.forEach(handler => handler()); // 在群里@所有人
}
}
// 康总的购房故事
const _event = new EventEmitter();
function kang() {
console.log('我是康总,快给我房子!');
}
_event.on('hasHouse', kang); // 康总加群
_event.emit('hasHouse'); // 中介发通知 → 输出:我是康总,快给我房子!
二、基础实现的三要素
1. 事件存储中心
this.eventList = {}
就像中介的客户登记表:
javascript
{
'hasHouse': [kang, ji], // 想买房的人
'hasCar': [hao] // 想买车位的人
}
2. 订阅方法(on)
- 检查是否有对应"客户群"
- 没有就新建群聊
- 把回调函数加入群组
3. 发布方法(emit)
- 安全拷贝回调数组(避免执行过程中数组被修改)
- 遍历执行所有订阅者
三、让模式更健壮:取消订阅功能
现实中的康总可能会被其他中介挖走,这时候就需要off
方法:
javascript
class EventEmitter {
// ...原有代码
// 客户退群(取消订阅)
off(eventName, cb) {
const callbacks = this.eventList[eventName];
if (!callbacks) return;
const index = callbacks.indexOf(cb);
if (index !== -1) {
callbacks.splice(index, 1); // 从群里踢人
}
}
}
// 康总生气了要退群
_event.off('hasHouse', kang);
_event.emit('hasHouse'); // 此时不会再通知康总
面试注意点:
- 使用
indexOf
查找回调函数 - 检查
index !== -1
才执行删除 - 直接修改原数组(无需返回新数组)
四、高级特性:一次性订阅
有些客户就像渣男,只想听一次房源通知(比如特价房):
javascript
class EventEmitter {
// ...原有代码
once(eventName, cb) {
const onceWrapper = () => {
cb(); // 执行原回调
this.off(eventName, onceWrapper); // 自动退群
};
this.on(eventName, onceWrapper); // 先加群
}
}
// 康总只想听一次特价房通知
_event.once('discountHouse', () => {
console.log('康总:这套特价房我要了!');
});
_event.emit('discountHouse'); // 会触发
_event.emit('discountHouse'); // 不再触发
实现技巧:
- 包装原回调函数
- 在包装函数内执行
off
自我移除 - 使用闭包保持对原回调的引用
五、发布订阅的"亲兄弟":观察者模式
提到发布订阅模式,就不得不介绍它的"亲兄弟"------观察者模式。这对兄弟虽然相似,但性格迥异:
- 发布订阅像个社交达人,通过中间人(事件中心)牵线搭桥
- 观察者则像个控制狂,直接盯着目标对象的一举一动
让我们从一个简单的计数器例子开始,展示观察者模式如何实现数据和视图的自动同步:
JavaScript
<!DOCTYPE html>
<html>
<head>
<title>计数器观察者</title>
</head>
<body>
<h2 id="counter">0</h2>
<button id="increment">+1</button>
<script>
// 被观察的数据对象
const counterData = {
value: 0
};
// 中间变量防止递归
let _value = counterData.value;
// 观察者函数 - 更新DOM
function updateView(newValue) {
document.getElementById('counter').textContent = newValue;
}
// 使用defineProperty劫持数据
Object.defineProperty(counterData, 'value', {
get() {
return _value;
},
set(newValue) {
_value = newValue;
updateView(newValue); // 数据变化时自动更新视图
}
});
// 按钮点击事件
document.getElementById('increment').addEventListener('click', () => {
counterData.value += 1; // 修改数据会自动更新视图
});
</script>
</body>
</html>
六、观察者模式的核心原理
1.观察者模式的"甜蜜约束":数据劫持
老骥有了对象后,他的属性访问就变得不再"自由"了。让我们看看这个有趣的约束机制:
javascript
const 老骥 = {
a: 1 // 初始值
};
let _a = 老骥.a; // 中间变量
Object.defineProperty(老骥, 'a', {
get() {
console.log('【女友查岗】你为什么要访问这个属性?');
return _a; // 返回实际值
},
set(newVal) {
console.log(`【女友审批】想从 ${_a} 改成 ${newVal}?`);
if(newVal > 100) {
console.log('【女友驳回】数值太大,不允许!');
return;
}
_a = newVal; // 通过审批
console.log('【女友批准】修改成功~');
}
});
// 测试访问
console.log(老骥.a); // 触发getter
老骥.a = 50; // 正常修改
老骥.a = 200; // 被驳回
核心特点解析:
-
访问拦截:
- 每次读取属性都会经过
get
方法 - 可以在这里添加访问日志、权限检查等
- 每次读取属性都会经过
-
修改控制:
- 所有赋值操作都会触发
set
方法 - 可以在setter中添加业务规则验证
- 所有赋值操作都会触发
-
数据保护:
- 通过中间变量
_a
存储实际值 - 外部无法直接修改真实数据
- 通过中间变量
2. 观察者三要素
- Subject(主题) :被观察的数据对象
- Observer(观察者) :响应数据变化的函数
- Dependency(依赖) :建立数据与观察者的关系
3、与发布订阅模式的关键区别
特性 | 观察者模式 | 发布订阅模式 |
---|---|---|
耦合度 | 主题直接维护观察者列表 | 通过事件中心完全解耦 |
通信方式 | 直接调用观察者方法 | 通过事件名称匹配 |
典型应用 | Vue响应式系统 | Node.js EventEmitter |
七、设计模式总结:发布订阅与观察者模式
发布订阅模式和观察者模式都是实现对象间通信的重要设计模式。发布订阅模式通过事件中心解耦发布者和订阅者,就像房产中介连接买房者和卖房者,典型实现如Node.js的EventEmitter。观察者模式则更直接,被观察者维护观察者列表并在状态变化时主动通知,如同"控制狂"般直接监控对象变化,Vue的响应式系统正是基于此原理。
关键区别在于:发布订阅模式通过中介实现多对多通信,松耦合但稍复杂;观察者模式直接维护依赖关系,简单直接但耦合度较高。两种模式各有所长,发布订阅适合跨组件通信,观察者适合数据响应式场景。理解它们的实现原理和适用场景,是掌握现代前端框架的关键。