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

news2024/9/22 23:28:16

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

  • 1. 观察者模式
    • 1.1 什么是观察者模式
    • 1.2 代码实现
  • 2. 发布-订阅模式
    • 2.1 什么是发布-订阅模式
    • 2.2 代码实现
      • 2.2.1 基础版
      • 2.2.2 取消订阅
      • 2.2.3 订阅一次

1. 观察者模式

1.1 什么是观察者模式

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

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

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

  • 首先家长和老师(观察者)都会告诉小明他们对他的情绪状态很关注。(订阅事件)
  • 当小明(被观察者)的情绪发生变化时,他会通知所有注册过的观察者。例如,如果小明感到很开心,他会告诉父母和老师:“我今天心情很好!”;如果他感到沮丧,他也会告诉父母和老师:“我今天感觉不太好。”(通知变化)

这样父母和老师就能及时了解小明的情绪状态,当小明情绪低落时,他们可以给予他关心、安慰和支持。
在这个例子中,小明就是被观察者,而父母和老师都是观察者。

1.2 代码实现

下面就来简单实现一下它的代码。

class Subject {
  // 被观察者 学生
  constructor() {
    this.state = "happy";
    this.observers = []; // 存储所有的观察者
  }
  //新增观察者
  add(o) {
    this.observers.push(o);
  }
  // 更新状态
  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.state}`);
  }
}

//创建被观察者学生
let student = new Subject("学生");
//创建观察者父母和老师
let parent = new Observer("父母");
let teacher = new Observer("老师");
//给被观察者学生增加观察者
student.add(parent);
student.add(teacher);

student.setState("sad");
//亲爱的父母 通知您当前学生的状态是sad
//亲爱的老师 通知您当前学生的状态是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 wrapper = (...args) => {
      cb(args); 
      this.off(name, wrapper); 
    };
    this.on(name, wrapper);
  }
}

// 测试
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", "教室", "上课", "打架", "愤怒");

输出结果:

在这里插入图片描述

写作不易,你的一赞一评,就是我前行的最大动力。如有问题,欢迎指出!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/814871.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Windows11】家庭版开启组策略指南

目录 背景新建一个cmd文件运行运行结果 背景 Win11找不到gpedit.msc怎么办&#xff1f;有用户通过命令窗口想要去打开本地组策略的时候&#xff0c;系统突然弹出了一个错误提示&#xff0c;显示系统缺少了gpedit.msc导致无法开启本地组策略编辑器了。那么这个情况要怎么去进行…

【Web开发指南】如何用MyEclipse进行JavaScript开发?

由于MyEclipse中有高级语法高亮显示、智能内容辅助和准确验证等特性&#xff0c;进行JavaScript编码不再是一项繁琐的任务。 MyEclipse v2023.1.2离线版下载 JavaScript项目 在MyEclipse 2021及以后的版本中&#xff0c;大多数JavaScript支持都是开箱即用的JavaScript源代码…

【Minio怎么用】Minio上传图片并Vue回显

流程&#xff1a; 目录 1.文件服务器Minio的安装 1.1 下载Minio安装后&#xff0c;新建1个data文件夹。并在该安装目录cmd 敲命令。注意不要进错目录。依次输入 1.2 登录Minio网页端 1.3 先建1个桶&#xff08;buckets&#xff09;&#xff0c;点击create a bucket 2. Spr…

前端小练-仿掘金导航栏

文章目录 前言项目结构导航实现创作中心移动小球消息提示 完整代码 前言 闲的&#xff0c;你信嘛&#xff0c;还得开发一个基本的门户社区网站&#xff0c;来给到Hlang,不然我怕说工作量不够。那么这个的话&#xff0c;其实也很好办&#xff0c;主要是这个门户网站的UI写起来麻…

操作系统_进程与线程(三)

目录 3. 同步与互斥 3.1 同步与互斥的基本概念 3.1.1 临界资源 3.1.2 同步 3.1.3 互斥 3.2 实现临界区互斥的基本方法 3.2.1 软件实现方法 3.2.1.1 算法一&#xff1a;单标志法 3.2.1.2 算法二&#xff1a;双标志法先检查 3.2.1.3 算法三&#xff1a;双标志法后检查 …

HarmonyOS/OpenHarmony元服务开发-卡片使用自定义绘制能力

ArkTS卡片开放了自定义绘制的能力&#xff0c;在卡片上可以通过Canvas组件创建一块画布&#xff0c;然后通过CanvasRenderingContext2D对象在画布上进行自定义图形的绘制&#xff0c;如下示例代码实现了在画布的中心绘制了一个笑脸。 Entry Component struct Card { private c…

如何把几个视频合并在一起?视频合并方法分享

当我们需要制作一个比较长的视频时&#xff0c;将多个视频进行合并可以使得整个过程更加高效。此外&#xff0c;合并视频还可以避免出现“剪辑断层”的情况&#xff0c;使得视频内容更加连贯&#xff0c;更加容易被观众理解和接受。再有&#xff0c;合并视频还可以减少视频文件…

第三方电容笔支持随手写吗?性价比高的触控笔推荐

在日常生活中&#xff0c;电容笔的用途非常广泛&#xff0c;无论是配上笔记本&#xff0c;还是配上ipad&#xff0c;又或者是配上手机&#xff0c;都是非常好用的办公利器。首先要明确自己的需要&#xff0c;然后才能选择适合自己的产品。苹果Pencil因为具有特殊的重力压感&…

数据结构07:查找[C++][顺序、分块、折半查找]

图源&#xff1a;文心一言 考研笔记整理~&#x1f95d;&#x1f95d; 在数据结构和算法中&#xff0c;查找是一种常见的操作&#xff0c;它的目的是在一个数据集合中找到一个满足条件的元素。本文将介绍三种常用的查找方法&#xff0c;分别是顺序查找、折半查找和分块查找~&a…

Unity实现在3D模型标记

Canvas 模式是UI与3D混合模式&#xff08;Render modelScreen space-Camera) 实现在3D模型标记&#xff0c;旋转跟随是UI不在3D物体下 代码&#xff1a; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public clas…

记一次sql注入分析与绕过【一】

下面是来自今天的项目&#xff0c;简单记录一下 手工注入 加单引号sql报错 sql语句如下&#xff0c;可见参数id原本未被引号包裹 SELECT DISTINCT u.* FROM t_user u WHERE u.name like %1% and u.account like %1% and u.state ? order by id desc limit 0,20 多方尝试…

warnings.filterwarnings(“ignore“) 是干嘛的

在python中运行代码经常会遇到的情况是——代码可以正常运行但是会提示警告 那么如何来控制警告输出呢&#xff1f;其实很简单&#xff0c;python通过调用warnings模块中定义的warn()函数来发出警告。我们可以通过警告过滤器进行控制是否发出警告消息 import warnings warnin…

数字工厂管理系统的实施步骤是什么

数字工厂管理系统是一种基于数字化技术和智能化设备的工厂管理系统&#xff0c;它可以实现工厂的全面、实时、动态管理&#xff0c;提高生产效率、降低成本、保证产品质量。实施数字工厂管理系统需要一系列的实施步骤&#xff0c;下面就数字工厂管理系统的实施步骤进行详细说明…

postgresql selected, no connection解决办法|armitage连接不上

postgresql selected, no connection 数据库没有连接&#xff0c;手动连接数据库即可。 手动连接数据库 msf > db_connect msf:admin127.0.0.1/msf 还是不行。 说明&#xff0c;数据库都连不上&#xff0c;先解决这个问题。 正文 看过很多&#xff0c;也试了很多&#xf…

05 http连接处理(中)

05 http连接处理&#xff08;中&#xff09; 流程图与状态机 从状态机负责读取报文的一行&#xff0c;主状态机负责对该行数据进行解析&#xff0c;主状态机内部调用从状态机&#xff0c;从状态机驱动主状态机 主状态机 三种状态&#xff0c;标识解析位置 CHECK_STATE_RE…

Python工具箱系列(三十九)

使用zlib对数据进行压缩 现实世界中&#xff0c;大量存在着对数据压缩的需求。为此&#xff0c;python内置了zlib压缩库&#xff0c;可以方便的对任意对象进行压缩。 下述代码演示了对字符串进行压缩&#xff1a; import zlib# 压缩一段中文 originstr 神龟虽寿&#xff0c…

风靡朋友圈的妙鸭相机,到底用了哪些底层技术?

不知道大家近期的朋友圈有没有被和海马体、天真蓝如出一辙的AI写真刷屏&#xff01; 这些面若桃花、精致到头发丝、光影充满氛围感的写真都是一款叫“妙鸭相机”的小程序生成的&#xff01;只要9.9&#xff0c;就能体验999写真&#xff01; 虽然只要9.9&#xff0c;但生成的照片…

Mac电脑目录

System&#xff08;系统&#xff09;Applications&#xff08;应用程序&#xff09;应用程序目录&#xff0c;默认所有的GUI应用程序都安装在这里User&#xff08;用户&#xff09;存放用户的个人资料和配置。每个用户有自己的单独目录Library&#xff08;资料库&#xff09;系…

定义dubbo自己的异常过滤器

起因 发现这个问题的起因是前端联调接口的时候发现统一的异常处理没有发挥作用,我们定义的处理的异常类型为AppException(国际惯例继承于RuntimeException),但是Dubbo服务端实际返回的异常变成了RuntimeException,我们自定义的异常处理没有发生作用&#xff0c;导致前端报500异…

恒运资本:A股、港股全线爆发,沪指突破3300点,恒指重返2万点上方

7月31日&#xff0c;两市股指高开高走&#xff0c;沪指在金融、地产、酿酒等权重板块的带动下一举突破3300点。截至发稿&#xff0c;沪指、深成指、创业板指涨幅均超1%&#xff0c;上证50指数涨近2%。Wind数据显现&#xff0c;北向资金净买入超25亿元。 职业方面&#xff0c;券…