引言
- 假设你是一个UP主,你更新了一条视频,你的粉丝是不是应该需要立马收到消息
- 假设一件商品降价了,所有将这件商品加入购物车的用户,是不是应该收到app的信息
这些都是生活中十分常见的场景,这些场景都需要一个模式在code中体现。
没错,说的就是发布订阅模式
定义
发布订阅模式是一种对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,随后各自执行自己的动作
核心
事件的发布和订阅
代码怎么写?
我们需要声明一个类来代表这个模式,同时还需要它的一个实例对象_event
class EventEmitter{}
const _event = new EventEmitter()
如何订阅、发布事件?
在_event要是有相应方法该多好,所以
我们先写一个壳子放这里
class EventEmitter{
on(){} // 订阅事件
emit(){} // 发布事件
}
const _event = new EventEmitter()
订阅、发布的具体代码应该如何写?
考虑一:
- 在函数on(){}里,是不是应该放形参,那该放什么形参呢?
- 第一个形参:eventName(事件名)
- 第二个形参:callBack(谁要订阅这个事件?也就是订阅者)
- 在函数emit(){}里,你需要知道谁订阅了你的事件吗?
- 对,不需要,所以只需要一个形参:eventName(事件名)
class EventEmitter{
on(eventName,callBack){} // 订阅事件
emit(eventName){} // 发布事件
}
const _event = new EventEmitter()
考虑二:
- 我们如何将订阅的事件存起来
- 是不是应该是这样的:
- 事件xx:订阅者1、订阅者2......
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
on(eventName,callBack){} // 订阅事件
emit(eventName){} // 发布事件
}
const _event = new EventEmitter()
考虑三:
- 既然知道了要用数组来保存每位订阅者
- 所以,在函数on(eventName,callBack)接收实参时
- 如果订阅的事件不存在,我们就要创建一个数组,并把订阅者放进数组里存起来
- 如果订阅的事件存在,那我们直接把订阅者push进数组里
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
on(eventName,callBack){// 订阅事件
if(this.events[eventName]){
events[eventName].push(callBack);
}else{
events[eventName] = [callBack];
}
}
emit(eventName){} // 发布事件
}
const _event = new EventEmitter()
考虑四
- 订阅者是什么?
- 具体一点,订阅者其实就是一个函数
- 当得到消息后,要执行不同的功能(动作)
- 所以,事件的发布其实就是执行每一个存入事件数组里的函数
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
on(eventName,callBack){// 订阅事件
if(this.events[eventName]){
this.events[eventName].push(callBack);
}else{
this.events[eventName] = [callBack];
}
}
emit(eventName){// 发布事件
if(this.events[eventName]){
this.events[eventName].forEach(callBack => callBack());
}else{
console.log("事件不存在,无法发布");
}
}
}
const _event = new EventEmitter()
测试当前事件的订阅和发布能否正常执行
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
on(eventName,callBack){// 订阅事件
if(this.events[eventName]){
this.events[eventName].push(callBack);
}else{
this.events[eventName] = [callBack];
}
}
emit(eventName){// 发布事件
if(this.events[eventName]){
this.events[eventName].forEach(callBack => callBack());
}else{
console.log("事件不存在,无法发布");
}
}
}
const _event = new EventEmitter()
function yang() {
console.log('我是yang');
}
function xh() {
console.log('我是xh');
}
// yang 和 xh 都是订阅者,都订阅了事件'has_xh'
_event.on('has_xh', yang)
_event.on('has_xh', xh)
// 发布事件
_event.emit('has_xh')
- 可以看到,当发布事件后,该事件的所有函数(订阅者)都会收到消息,并执行各自的代码(行为)
既然能订阅事件,就要能取消订阅
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
// 订阅事件
on(eventName,callBack){
if(this.events[eventName]){
this.events[eventName].push(callBack);
}else{
this.events[eventName] = [callBack];
}
}
// 发布事件
emit(eventName){
if(this.events[eventName]){
this.events[eventName].forEach(callBack => callBack());
}else{
console.log("事件不存在,无法发布");
}
}
// 取消某一个订阅者的订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback)
} else {
console.log(`没有事件:${eventName}`)
}
}
}
const _event = new EventEmitter()
function yang() {
console.log('我是yang');
}
function xh() {
console.log('我是xh');
}
// yang 和 xh 都是订阅者,都订阅了事件'has_xh'
_event.on('has_xh', yang)
_event.on('has_xh', xh)
// 取消订阅了xh的事件'has_xh'
_event.off('has_xh', xh)
// 发布事件
_event.emit('has_xh')
输出结果:
如果订阅者只想订阅一次呢?
考虑:
- 我们要在EventEmitter类里新增一个方法
- 订阅、发布后立即取消订阅
class EventEmitter{
constructor() {
this.events = {
// "事件1":[订阅者1,订阅者2]
}
}
// 订阅事件
on(eventName,callBack){
if(this.events[eventName]){
this.events[eventName].push(callBack);
}else{
this.events[eventName] = [callBack];
}
}
// 发布事件
emit(eventName){
if(this.events[eventName]){
this.events[eventName].forEach(callBack => callBack());
}else{
console.log("事件不存在,无法发布");
}
}
// 取消某一个订阅者的订阅事件
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback)
} else {
console.log(`没有事件:${eventName}`)
}
}
// 一次性订阅
once(eventName, callback){
const wrap = () =>{
// 发布事件(执行函数)
callback()
// 取消订阅
this.off(eventName, wrap)
}
// 订阅事件
this.on(eventName, wrap)
}
}
const _event = new EventEmitter()
function yang() {
console.log('我是yang');
}
function xh() {
console.log('我是xh');
}
// yang 和 xh 都是订阅者,都订阅了事件'has_xh'
_event.on('has_xh', yang)
_event.on('has_xh', xh)
// xh一次性订阅事件'has_xh'
_event.once('has_xh', xh)
// 两次发布事件'has_xh'
_event.emit('has_xh')
_event.emit('has_xh')
输出结果:只输出了一遍,说明确实是一次性订阅
用订阅发布模式处理异步——牵着面试官鼻子走的关键一步
- A()是耗时的代码,所以会被挂起
- 先执行_event.on('next', B),也就是先订阅事件'next'
- 完成事件的订阅后,执行A()里的事件发布
- 得到的输出效果:
- 1s后打印 A
- 2s后打印 B
- 这样就完成了先执行耗时代码A(),再执行耗时代码B()
- 解释一下:为什么要在函数B里用定时器?
- 是为了让效果更明显,不然A B是同时输出的,看起来好像没啥区别
// 此处省略上面的EventEmitter类
function A() {
setTimeout(() => {
console.log('A')
_event.emit('next')
}, 1000)
}
function B() {
setTimeout(() => {
console.log('B')
}, 1000)
}
A();
let _event = new EventEmitter()
_event.on('next', B)
面试官的鼻子怎么牵?
假设面试官问:请你聊一聊异步的处理方式
可以这么回答:
- 因为 JS 默认是以单线程来执行的
- 当出现需要同步执行的代码和异步执行的代码时,会通过事件循环来处理。
- 事件循环的机制是:先执行同步代码,再执行异步代码。
- 可能会影响代码的执行顺序,所以需要处理异步代码
- 一开始使用回调函数
- 但是当项目越来越大,回调越嵌越深,代码的可读性和可维护性都会降低。
- 所以需要使用 Promise 来处理异步代码。
- 不美观,所以采用async/await
- 完全可以用发布订阅的方式来处理异步代码。
- 再聊聊刚刚案例里的函数A和函数B
你都这么聊了,面试官当然要考考你:请你手搓一个发布订阅模式
- 这个时候,你不就可以给他露一手了吗?
观察者模式
- 和发布订阅模式是对双胞胎,但是略有区别
- 这个不重要,但是也得聊一聊
在聊观察者模式之前,先聊一聊Object.defineProperty()方法
1. 基本使用
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'John',
writable: true,
enumerable: true,
configurable: true
});
console.log(obj.name); // "John"
2. 不可写属性
const obj = {};
Object.defineProperty(obj, 'readOnly', {
value: 42,
writable: false
});
obj.readOnly = 100; // 静默失败,严格模式下会报错
console.log(obj.readOnly); // 42
3. 不可枚举属性
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 'secret',
enumerable: false
});
console.log(obj.hidden); // "secret"
console.log(Object.keys(obj)); // []
4. 使用getter和setter
const obj = {};
let internalValue = '';
Object.defineProperty(obj, 'greeting', {
get() {
return internalValue;
},
set(value) {
internalValue = 'Hello, ' + value;
},
enumerable: true,
configurable: true
});
obj.greeting = 'World';
console.log(obj.greeting); // "Hello, World"
5. 不可配置属性
const obj = {};
Object.defineProperty(obj, 'fixed', {
value: 'cannot change',
configurable: false
});
delete obj.fixed; // false
console.log(obj.fixed); // "cannot change"
// 严格模式下会抛出错误
在观察者模式中的应用
<!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>
<h2>1</h2>
<div id="app">
<h1>{count}</h1>
</div>
<button>点击</button>
<script>
const h2 = document.querySelector('h2')
const btn = document.querySelector('button')
let obj = {
count: 1
}
let num = obj.count // 防止递归爆栈
function observer(val) {
//找出页面上所有 用到了 count 这个变量的dom 结构 -- 找到订阅者
// 将订阅者的内容替换为新的值
h2.innerHTML = val
}
Object.defineProperty(obj, 'count', {
get() {
return num
},
set(val) {
observer(val)
num = val
}
})
btn.addEventListener('click', () => {
obj.count++
})
</script>
</body>
</html>
发布订阅模式与观察者模式的区别
虽然两者均用于解耦,但存在关键差异:
- 观察者模式:发布者直接维护订阅者列表,属于松耦合的一对多关系。
- 发布-订阅模式:通过消息代理完全解耦,发布者和订阅者互不知晓对方存在,支持多对多通信
- 你也可以说他俩没区别