JavaScript 简单实现观察者模式和发布-订阅模式

简介: JavaScript 简单实现观察者模式和发布-订阅模式

1. 观察者模式


1.1 什么是观察者模式


概念:观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。


如何理解这句话呢?来举个生活中的例子


学生小明情绪比较容易波动,所以当小明的情绪发生变化时,父母和老师希望及时获得通知,以便可以采取适当的措施来帮助他。


首先家长和老师(观察者)都会告诉小明他们对他的情绪状态很关注。(订阅事件)

当小明(被观察者)的情绪发生变化时,他会通知所有注册过的观察者。例如,如果小明感到很开心,他会告诉父母和老师:“我今天心情很好!”;如果他感到沮丧,他也会告诉父母和老师:“我今天感觉不太好。”(通知变化)

这样父母和老师就能及时了解小明的情绪状态,当小明情绪低落时,他们可以给予他关心、安慰和支持。

在这个例子中,小明就是被观察者,而父母和老师都是观察者。


1.2 代码实现


观察者模式有何应用呢?


Vue的响应式就是基于观察者模式的,下面就来简单实现一下它的代码。


// 被观察者 学生
class Subject {
  constructor() {
    this.state = "happy";
    this.observers = []; // 存储所有的观察者
  }
  //新增观察者
  add(o) {
    this.observers.push(o);
  }
  //获取状态
  getState() {
    return this.state;
  }
  // 更新状态并通知
  setState(newState) {
    this.state = newState;
    this.notify();
  }
  //通知所有的观察者
  notify() {
    this.observers.forEach((o) => o.update(this));
  }
}
// 观察者 父母和老师
class Observer {
  constructor(name) {
    this.name = name;
  }
  //更新
  update(student) {
    console.log(`亲爱的${this.name} 通知您当前学生的状态是${student.getState()}`);
  }
}
let student = new Subject();
let parent = new Observer("父母");
let teacher = new Observer("老师");
//添加观察者
student.add(parent);
student.add(teacher);
//设置被观察者的状态
student.setState("sad");


2. 发布-订阅模式


2.1 什么是发布-订阅模式


发布订阅模式跟观察者模式很像,它们其实都有发布者和订阅者,但是他们是有区别的:


观察者模式的发布和订阅是互相依赖的

发布订阅模式的发布和订阅是不互相依赖的,因为有一个统一调度中心

为了更好区分这两种设计模式,接着上述例子。


所有老师都希望订阅小明的情绪状态,他们向情绪监测系统注册自己,来时刻关注小明的情绪。(向调度中心订阅事件)

当小明的情绪发生变化时,情绪监测系统会将消息发布给所有订阅了小明情绪状态的老师。例如,如果小明在上课时感到烦躁,情绪监测系统会发布消息给老师:“小明情绪不稳定,请关注他的情绪变化。”(调度中心通知变化)

通过发布订阅模式,小明不需要直接告诉每位老师他的情绪状态,而是通过情绪监测系统自动发布消息给所有订阅了他情绪状态的老师。这种发布者不直接接触到订阅者的模式,就是发布订阅模式。

那么发布订阅模式有何应用呢?

Vue的EventBus事件总线其实就是用了发布订阅模式。用法如下:

1.创建全局事件总线

// main.js
import Vue from "vue"
Vue.prototype.$bus = new Vue()

2.通过on订阅事件

//组件A
export default{
    mounted(){
        // 监听事件的触发
        this.$bus.$on("sendMsg", data => {
            console.log(data)//身体健康
        })
    },
    beforeDestroy(){
        // 取消监听
        this.$bus.$off("sendMsg")
    }
}

3.通过emit发布事件

//组件B
<template>
    <button @click="handlerClick">点击发送数据</button>
</template>
export default{
    methods:{
        handlerClick(){
            this.$bus.$emit("sendMsg", "身体健康")
        }
    }
}

了解了EventBus的使用后,那么接下来就来手动实现一个EventBus。


2.2 代码实现


2.2.1 基础版


实现目标:使用 $on 订阅事件,使用 $emit 发布事件。

主要思路:


创建一个缓存列表对象,存放订阅的事件名和回调

on 方法用来把回调函数都加到缓存列表中(订阅者注册事件到调度中心)

emit方法根据事件名去逐个执行对应缓存列表中的函数(发布者发布事件到调度中心)

class EventBus {
  constructor() {
    // 缓存列表,用来存放注册的事件与回调
    this.cache = {};
  }
  // 订阅事件
  on(name, cb) {
    // 如果当前事件没有订阅过,就给事件创建一个队列
    if (!this.cache[name]) {
      this.cache[name] = []; //由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
    }
    this.cache[name].push(cb); 
  }
  // 触发事件
  emit(name, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.cache[name]) {
      // 逐个调用队列里的回调函数
      this.cache[name].forEach((callback) => {
        callback(...args);
      });
    }
  }
}
// 测试
let eventBus = new EventBus();
// 订阅事件
eventBus.on("teacherName1", (pos, state) => {
  console.log(`订阅者小陈老师,小明同学当前在${pos},心情状态是${state}`);
});
eventBus.on("teacherName1", (pos, state) => {
  console.log(`订阅者小陈老师,小明同学当前在${pos},心情状态是${state}`);
});
eventBus.on("teacherName2", (pos, state) => {
  console.log(`订阅者小李老师,小明同学当前在${pos},心情状态是${state}`);
});
// 发布事件
eventBus.emit("teacherName1", "教室", "伤心");
eventBus.emit("teacherName2", "操场", "开心");

输出结果:



2.2.2 取消订阅


实现目标:增加 off 方法取消订阅。


off 方法:找到当前取消事件名对应的函数队列中相应回调,进行删除

class EventBus {
  constructor() {
    // 缓存列表,用来存放注册的事件与回调
    this.cache = {};
  }
  // 订阅事件
  on(name, cb) {
    // 如果当前事件没有订阅过,就给事件创建一个队列
    if (!this.cache[name]) {
      this.cache[name] = []; //由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
    }
    this.cache[name].push(cb); 
  }
  // 触发事件
  emit(name, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.cache[name]) {
      // 逐个调用队列里的回调函数
      this.cache[name].forEach((callback) => {
        callback(...args);
      });
    }
  }
  // 取消订阅
  off(name, cb) {
    const callbacks = this.cache[name]; 
    const index = callbacks.indexOf(cb); 
    if (index !== -1) {
      callbacks.splice(index, 1); 
    }
  }
}
// 测试
let eventBus = new EventBus();
let event1 = function (...args) {
  console.log(`通知1-订阅者小陈老师,小明同学当前心情状态:${args}`)
};
let event2 = function (...args) {
  console.log(`通知2-订阅者小陈老师,小明同学当前心情状态:${args}`)
};
// 订阅事件
eventBus.on("teacherName1", event1);
eventBus.on("teacherName1", event2);
// 取消订阅事件1
eventBus.off('teacherName1', event1);
// 发布事件
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
eventBus.emit("teacherName2", "教室", "上课", "打架", "愤怒");

输出结果:



2.2.3 订阅一次


实现目标:增加 once 方法只订阅一次。


once 方法只监听一次,执行完第一次回调函数后,自动删除当前订阅事件

class EventBus {
  constructor() {
    // 缓存列表,用来存放注册的事件与回调
    this.cache = {};
  }
  // 订阅事件
  on(name, cb) {
    // 如果当前事件没有订阅过,就给事件创建一个队列
    if (!this.cache[name]) {
      this.cache[name] = []; //由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
    }
    this.cache[name].push(cb); 
  }
  // 触发事件
  emit(name, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.cache[name]) {
      // 逐个调用队列里的回调函数
      this.cache[name].forEach((callback) => {
        callback(...args);
      });
    }
  }
  // 取消订阅
  off(name, cb) {
    const callbacks = this.cache[name]; 
    const index = callbacks.indexOf(cb); 
    if (index !== -1) {
      callbacks.splice(index, 1); 
    }
  }
  // 只订阅一次
  once(name, cb) {
    // 执行完第一次回调函数后,自动删除当前订阅事件
    const fn = (...args) => {
      cb(...args); 
      this.off(name, fn); 
    };
    this.on(name, fn);
  }
}
// 测试
let eventBus = new EventBus();
let event1 = function (...args) {
  console.log(`通知1-订阅者小陈老师,小明同学当前心情状态:${args}`)
};
// 订阅事件,只订阅一次
eventBus.once("teacherName1", event1);
// 发布事件
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");
eventBus.emit("teacherName1", "教室", "上课", "打架", "愤怒");

输出结果:

相关文章
|
4月前
|
存储 安全 JavaScript
云计算浪潮中的网络安全之舵探索Node.js中的异步编程模式
【8月更文挑战第27天】在数字化时代的风帆下,云计算如同一片广阔的海洋,承载着企业与个人的数据梦想。然而,这片海洋并非总是风平浪静。随着网络攻击的波涛汹涌,如何确保航行的安全成为了每一个船员必须面对的挑战。本文将探索云计算环境下的网络安全策略,从云服务的本质出发,深入信息安全的核心,揭示如何在云海中找到安全的灯塔。
|
11天前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
|
1月前
|
前端开发 JavaScript UED
探索JavaScript的异步编程模式
【10月更文挑战第40天】在JavaScript的世界里,异步编程是一道不可或缺的风景线。它允许我们在等待慢速操作(如网络请求)完成时继续执行其他任务,极大地提高了程序的性能和用户体验。本文将深入浅出地探讨Promise、async/await等异步编程技术,通过生动的比喻和实际代码示例,带你领略JavaScript异步编程的魅力所在。
25 1
|
2月前
|
前端开发 JavaScript UED
探索JavaScript中的异步编程模式
【10月更文挑战第21天】在数字时代的浪潮中,JavaScript作为一门动态的、解释型的编程语言,以其卓越的灵活性和强大的功能在Web开发领域扮演着举足轻重的角色。本篇文章旨在深入探讨JavaScript中的异步编程模式,揭示其背后的原理和实践方法。通过分析回调函数、Promise对象以及async/await语法糖等关键技术点,我们将一同揭开JavaScript异步编程的神秘面纱,领略其带来的非阻塞I/O操作的魅力。让我们跟随代码的步伐,开启一场关于时间、性能与用户体验的奇妙之旅。
|
1月前
|
前端开发 JavaScript UED
探索JavaScript的异步编程模式
【10月更文挑战第33天】在JavaScript的世界里,异步编程是提升应用性能和用户体验的关键。本文将带你深入理解异步编程的核心概念,并展示如何在实际开发中运用这些知识来构建更流畅、响应更快的Web应用程序。从回调函数到Promises,再到async/await,我们将一步步解锁JavaScript异步编程的秘密,让你轻松应对各种复杂的异步场景。
|
2月前
|
JavaScript 前端开发 API
探索Node.js中的异步编程模式
【10月更文挑战第4天】在JavaScript的世界中,异步编程是提升应用性能和用户体验的关键。本文将深入探讨Node.js中异步编程的几种模式,包括回调函数、Promises、async/await,并分享如何有效利用这些模式来构建高性能的后端服务。
|
2月前
|
JavaScript 前端开发 调度
探索Node.js中的异步编程模式
在Node.js的世界里,异步编程是核心。本文将带你深入了解异步编程的精髓,通过代码示例和实际案例分析,我们将一起掌握事件循环、回调函数、Promises以及async/await等关键概念。准备好迎接挑战,让你的Node.js应用飞起来!
|
2月前
|
JavaScript 前端开发 开发者
探索Node.js中的异步编程模式
【9月更文挑战第33天】在JavaScript的后端领域,Node.js凭借其非阻塞I/O和事件驱动的特性,成为高性能应用的首选平台。本文将深入浅出地探讨Node.js中异步编程的核心概念、Promise对象、Async/Await语法以及它们如何优化后端开发的效率和性能。
30 7
|
3月前
|
JavaScript 前端开发 开发者
探索Node.js中的异步编程模式
【9月更文挑战第15天】在Node.js的世界中,“一切皆异步”不仅是一句口号,更是其设计哲学的核心。本文将带你深入理解Node.js中异步编程的几种主要模式,包括经典的回调函数、强大的Promise对象、以及简洁的async/await结构。我们将通过实例代码来展示每种模式的使用方式和优缺点,帮助你更好地掌握Node.js异步编程的精髓。无论你是Node.js新手还是有一定经验的开发者,这篇文章都能给你带来新的启示和思考。让我们一起开启Node.js异步编程的探索之旅吧!
|
3月前
|
JavaScript 前端开发 中间件
深入浅出Node.js中间件模式
【9月更文挑战第13天】本文将带你领略Node.js中间件模式的魅力,从概念到实战,一步步揭示如何利用这一强大工具简化和增强你的Web应用。我们将通过实际代码示例,展示中间件如何在不修改原有代码的情况下,为请求处理流程添加功能层。无论你是前端还是后端开发者,这篇文章都将为你打开一扇通往更高效、更可维护代码的大门。