0基础进大厂,第18天,教你如何牵着面试官鼻子走——细聊、手搓发布订阅模式

70 阅读6分钟

引言

  • 假设你是一个UP主,你更新了一条视频,你的粉丝是不是应该需要立马收到消息
  • 假设一件商品降价了,所有将这件商品加入购物车的用户,是不是应该收到app的信息
    这些都是生活中十分常见的场景,这些场景都需要一个模式在code中体现。
    没错,说的就是发布订阅模式

定义

发布订阅模式是一种对象间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,随后各自执行自己的动作

核心

事件的发布和订阅

代码怎么写?

我们需要声明一个类来代表这个模式,同时还需要它的一个实例对象_event

class EventEmitter{}
const _event = new EventEmitter()

如何订阅、发布事件?

在_event要是有相应方法该多好,所以
我们先写一个壳子放这里

class EventEmitter{
    on(){} // 订阅事件
    emit(){} // 发布事件
}
const _event = new EventEmitter()

订阅、发布的具体代码应该如何写?

考虑一:
  1. 在函数on(){}里,是不是应该放形参,那该放什么形参呢?
  2. 第一个形参:eventName(事件名)
  3. 第二个形参:callBack(谁要订阅这个事件?也就是订阅者)
  4. 在函数emit(){}里,你需要知道谁订阅了你的事件吗?
  5. 对,不需要,所以只需要一个形参:eventName(事件名)
class EventEmitter{
    on(eventName,callBack){} // 订阅事件
    emit(eventName){} // 发布事件
}
const _event = new EventEmitter()
考虑二:
  1. 我们如何将订阅的事件存起来
  2. 是不是应该是这样的:
  3. 事件xx:订阅者1、订阅者2......
class EventEmitter{
    constructor() {
        this.events = {
          // "事件1":[订阅者1,订阅者2]
        } 
    }
    on(eventName,callBack){} // 订阅事件
    emit(eventName){} // 发布事件
}
const _event = new EventEmitter()
考虑三:
  1. 既然知道了要用数组来保存每位订阅者
  2. 所以,在函数on(eventName,callBack)接收实参时
  3. 如果订阅的事件不存在,我们就要创建一个数组,并把订阅者放进数组里存起来
  4. 如果订阅的事件存在,那我们直接把订阅者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()
考虑四
  1. 订阅者是什么?
  2. 具体一点,订阅者其实就是一个函数
  3. 当得到消息后,要执行不同的功能(动作)
  4. 所以,事件的发布其实就是执行每一个存入事件数组里的函数
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')
  • 可以看到,当发布事件后,该事件的所有函数(订阅者)都会收到消息,并执行各自的代码(行为) image.png

既然能订阅事件,就要能取消订阅

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')

输出结果: image.png

如果订阅者只想订阅一次呢?

考虑:
  1. 我们要在EventEmitter类里新增一个方法
  2. 订阅、发布后立即取消订阅
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')

输出结果:只输出了一遍,说明确实是一次性订阅

image.png

用订阅发布模式处理异步——牵着面试官鼻子走的关键一步

  • 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)

面试官的鼻子怎么牵?

假设面试官问:请你聊一聊异步的处理方式

可以这么回答:

  1. 因为 JS 默认是以单线程来执行的
  2. 当出现需要同步执行的代码和异步执行的代码时,会通过事件循环来处理。
  3. 事件循环的机制是:先执行同步代码,再执行异步代码。
  4. 可能会影响代码的执行顺序,所以需要处理异步代码
  5. 一开始使用回调函数
  6. 但是当项目越来越大,回调越嵌越深,代码的可读性和可维护性都会降低。
  7. 所以需要使用 Promise 来处理异步代码。
  8. 不美观,所以采用async/await
  9. 完全可以用发布订阅的方式来处理异步代码。
  10. 再聊聊刚刚案例里的函数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>

发布订阅模式与观察者模式的区别

虽然两者均用于解耦,但存在关键差异:

  • 观察者模式​:发布者直接维护订阅者列表,属于松耦合的一对多关系。
  • 发布-订阅模式​:通过消息代理完全解耦,发布者和订阅者互不知晓对方存在,支持多对多通信
  • 你也可以说他俩没区别