百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

前端最常见的设计模式——发布订阅篇

toyiye 2024-06-21 12:06 9 浏览 0 评论

概念

有的人说:发布—订阅模式又叫观察者模式,但是我不这么认为,感觉它们之间其实还是有区别的。

观察者模式:

  1. 结构: 在观察者模式中,通常包含两个主要角色 — 观察者(Observers)和被观察者(Subject)。
  2. 关系: 观察者直接订阅被观察者。被观察者维护一个观察者列表,并在状态变化时通知所有观察者。
  3. 通知方式: 被观察者主动向观察者推送信息,通常是调用观察者的方法来进行通知。

发布-订阅模式:

  1. 结构: 在发布-订阅模式中,有一个中介者(通常称为“事件总线”或“消息队列”)来管理订阅者和发布者之间的关系。
  2. 关系: 发布者和订阅者不直接耦合,它们通过中介者进行通信。发布者将消息发送到中介者,然后中介者将消息传递给所有订阅者。
  3. 通知方式: 订阅者通过向中介者注册感兴趣的事件或主题,中介者在接收到消息后负责将消息分发给所有订阅者。

关键区别:

  1. 直接耦合 vs 间接耦合: 观察者模式中,观察者和被观察者直接耦合;而发布-订阅模式中,发布者和订阅者通过中介者间接耦合。
  2. 通信方式: 观察者模式中,通常是被观察者直接通知观察者;发布-订阅模式中,通过中介者进行通信,发布者只需向中介者发布消息,而不需要直接通知订阅者。

观察者模式更适合简单的一对多通信,而发布-订阅模式则更适用于松散耦合的场景,尤其是在复杂系统中需要处理多个事件和消息的情况。

发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

在 JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。

作用

可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。

可以取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

从架构上来看,无论是 MVC 还是 MVVM,都少不了发布—订阅模式的参与,而且 JavaScript 本身也是一门基于事件驱动的语言。

简单的理解

DOM 事件

下边这段代码就是利用了发布订阅模式。

document.getElementById("myBtn").addEventListener("click", function(){
  alert("Hello World!");
});

简易发布订阅

// on 是订阅 emit 是发布
let e = {
    // 存订阅
    _callback: [],
    on(callback) {
        // 订阅一件事 当这件事发生的时候 触发对应的函数
        // 订阅 就是将函数放到数组中
        this._callback.push(callback);
    },
    emit(value) {
        this._callback.forEach(method => {
            method(value);
        });
    }
};
// 订阅
e.on(function (value) {
    console.log(value + ":张三的订阅");
});
// 订阅
e.on(function (value) {
    console.log(value + ":李四的订阅");
});
// 订阅
e.on(function (value) {
    console.log(value + ":王五的订阅");
});
// 发布
e.emit('发布')

自定义事件

let salesOffices = { // 定义售楼处
  clientList: {},  // 缓存列表,存放订阅者的回调函数 
  
  listen(key, fn) {
    if (!this.clientList[key]) { // 如果还没有订阅过此类消息,给该类消息创建一个缓存列表 
      this.clientList[key] = []; 
    }
    this.clientList[key].push(fn); // 订阅的消息添加进消息缓存列表 
  }, 
  
  trigger() { // 发布消息 
    let key = Array.prototype.shift.call(arguments) // 取出消息类型 
    let fns = this.clientList[key]; // 取出该消息对应的回调函数集合 
        
    if (!fns || fns.length === 0) { // 如果没有订阅该消息,则返回 
      return false; 
    } 
    
    for(let fn of fns){ // (2) // arguments是发布消息时附送的参数 
      fn.apply(this, arguments);  
    }    
  }
};

// 例子
salesOffices.listen('squareMeter88', price => console.log(`价格= ${price}`));

salesOffices.listen('squareMeter110', price => console.log(`价格= ${price}`)); 

salesOffices.trigger('squareMeter88', 2000000);

salesOffices.trigger('squareMeter110', 3000000);

通用的实现

通用的一种封装,实现了订阅、发布、取消

const event = {
    clientList: [],
    listen: function( key, fn ){
      if (!this.clientList[key] ){
          this.clientList[key] = [];
      }
      this.clientList[key].push(fn);    // 订阅的消息添加进缓存列表
    },
    trigger: function(){
      const key = Array.prototype.shift.call( arguments )    // (1);
      const fns = this.clientList[ key ]

      if (!fns || fns.length === 0){    // 如果没有绑定对应的消息
          return false;
      }

      for(let i = 0, fn; fn = fns[i++];){
          fn.apply(this, arguments);    // (2) // arguments是trigger时带上的参数
      }
    },
    remove: function(key, fn){
        let fns = this.clientList[key];

        if (!fns){    // 如果key对应的消息没有被人订阅,则直接返回
          return false;
        }
        
        if (!fn){    // 如果没有传入具体的回调函数,表示需要取消key对应消息的所有订阅
          fns && (fns.length = 0);
        } else {
          for (let l = fns.length -1; l >=0; l--){ // 反向遍历订阅的回调函数列表
              let _fn = fns[l];
              if (_fn === fn){
                  fns.splice(l, 1);    // 删除订阅者的回调函数
              }
          }
        }
    }
};

const installEvent = function( obj ){
    obj = { ...obj, ...event }
};

let salesOffices = {};
installEvent(salesOffices);

salesOffices.listen( 'squareMeter88', fn1 = function(price){    // 小明订阅消息
    console.log('价格= ' + price);
});

salesOffices.listen( 'squareMeter100', fn2 = function(price){     // 小红订阅消息
    console.log('价格= ' + price );
});

salesOffices.remove('squareMeter88', fn1);    // 删除小明的订阅

salesOffices.trigger('squareMeter88', 2000000);    // 输出:2000000
salesOffices.trigger('squareMeter100', 3000000);    // 输出:3000000

谁用了?

在前端开发中,有许多库和框架使用了发布-订阅模式或提供了类似的机制,以便实现组件通信、事件处理、状态管理等功能。

以下是一些常见的前端库和框架,它们使用了发布-订阅模式或相关的设计模式:

PubSubJS

PubSubJS 是一个实现发布-订阅模式的 JavaScript 库。

PubSubJS 是一个独立的 JavaScript 模块,它提供了发布-订阅模式的实现。它可以在任何 JavaScript 环境中使用,包括浏览器和 Node.js。

以下是 src/pubsub.js 文件的核心部分的简要解释:

  1. 模块模式: 代码使用了 JavaScript 的模块模式,通过 IIFE(立即调用函数表达式)封装了整个库。
(function (root, factory) {
  // ...
}(this, function () {
  // ...
}));
  1. 核心对象: 主要的 PubSub 对象,并且有一些私有的辅助函数和变量。
var PubSub = {};

// 发布
PubSub.publish = function( message, data ){};

PubSub.publishSync = function( message, data ){};

// 订阅
PubSub.subscribe = function( message, func ){};

// 订阅所有
PubSub.subscribeAll = function( func ){};

// 允许只订阅一次的消息
PubSub.subscribeOnce = function( message, func ){};

// 用于清除所有订阅
PubSub.clearAllSubscriptions = function clearAllSubscriptions(){};

PubSub.clearSubscriptions = function clearSubscriptions(topic){};

PubSub.countSubscriptions = function countSubscriptions(topic){};

PubSub.getSubscriptions = function getSubscriptions(topic){};

// 取消订阅
PubSub.unsubscribe = function(value){};

var messages = {};

Vue

Vue 提供了一个简单的事件系统,通过 vm.$emit 发布事件,vm.$on 订阅事件。这种机制类似于发布-订阅模式,允许组件之间进行松散耦合的通信。

在 Vue 中使用发布-订阅模式的例子:

  1. 使用 EventBus: 你可以创建一个简单的 EventBus,用于在不同组件之间进行通信。
// EventBus.js
import Vue from 'vue';
export const EventBus = new Vue();

// ComponentA.vue
import { EventBus } from './EventBus';
export default {
  methods: {
    sendMessage() {
      EventBus.$emit('message', 'Hello from ComponentA!');
    }
  }
}

// ComponentB.vue
import { EventBus } from './EventBus';
export default {
  created() {
    EventBus.$on('message', message => {
      console.log('Received message in ComponentB:', message);
    });
  }
}
  1. 使用 provide/inject: 如果你在一个深层嵌套的组件结构中,可以使用 provideinject 来提供一个全局的事件总线。
// EventBus.js
import Vue from 'vue';
export const EventBus = new Vue();

// App.vue
import { EventBus } from './EventBus';
export default {
  provide: {
    eventBus: EventBus
  }
}

// ComponentA.vue
export default {
  inject: ['eventBus'],
  methods: {
    sendMessage() {
      this.eventBus.$emit('message', 'Hello from ComponentA!');
    }
  }
}

// ComponentB.vue
export default {
  inject: ['eventBus'],
  created() {
    this.eventBus.$on('message', message => {
      console.log('Received message in ComponentB:', message);
    });
  }
}

React

在 React 中,并没有像 Vue 那样直接提供发布-订阅模式的特定实现。React 更倾向于使用单向数据流和通过 props 和回调函数进行组件通信。

然而,如果你需要在 React 中实现发布-订阅模式,你可以借助全局状态管理库(如Redux)、或使用第三方发布订阅库 PubSubJS 、或使用 React 的 Context API 。

以下是一个使用 React Context API 来实现发布-订阅模式的简单例子:

// EventBusContext.js
import { createContext, useContext } from 'react';

const EventBusContext = createContext();

export const useEventBus = () => {
  return useContext(EventBusContext);
};

export const EventBusProvider = ({ children }) => {
  const listeners = {};

  const subscribe = (eventName, callback) => {
    if (!listeners[eventName]) {
      listeners[eventName] = [];
    }
    listeners[eventName].push(callback);

    return () => {
      listeners[eventName] = listeners[eventName].filter(cb => cb !== callback);
    };
  };

  const publish = (eventName, data) => {
    if (listeners[eventName]) {
      listeners[eventName].forEach(callback => {
        callback(data);
      });
    }
  };

  const value = {
    subscribe,
    publish
  };

  return (
    <EventBusContext.Provider value={value}>
      {children}
    </EventBusContext.Provider>
  );
};


// ComponentA.js
import React from 'react';
import { useEventBus } from './EventBusContext';

const ComponentA = () => {
  const eventBus = useEventBus();

  const sendMessage = () => {
    eventBus.publish('message', 'Hello from ComponentA!');
  };

  return (
    <div>
      <button onClick={sendMessage}>Send Message</button>
    </div>
  );
};

export default ComponentA;
// ComponentB.js
import React, { useEffect } from 'react';
import { useEventBus } from './EventBusContext';

const ComponentB = () => {
  const eventBus = useEventBus();

  useEffect(() => {
    const unsubscribe = eventBus.subscribe('message', message => {
      console.log('Received message in ComponentB:', message);
    });

    return () => {
      unsubscribe();
    };
  }, [eventBus]);

  return <div>ComponentB</div>;
};

export default ComponentB;

Redux

在 Redux 中,并没有直接提供经典的发布-订阅模式,Redux 引入了一种单向数据流的架构,其中状态的变化通过派发(dispatch)动作来进行管理。

Redux 中有一些机制可以达到一些类似发布-订阅的效果:

  1. 中间件(Middleware): Redux 中的中间件可以截获派发的动作,进行一些额外的操作,然后将动作传递给下一个中间件或 Redux store。
const loggerMiddleware = store => next => action => {
  console.log('Dispatching action:', action);
  return next(action);
};

const store = createStore(
  rootReducer,
  applyMiddleware(loggerMiddleware)
);
  1. Store.subscribe 方法: Redux 的 store 对象提供了一个 subscribe 方法,该方法接受一个回调函数,每当状态发生变化时都会被调用。
const store = createStore(rootReducer);

const unsubscribe = store.subscribe(() => {
  console.log('State changed:', store.getState());
});

// 当不再需要订阅时,调用 unsubscribe 取消订阅
unsubscribe();

jQuery

jQuery 中的事件系统是一个经典的发布-订阅模式的实现。通过 jQuery 的事件系统,你可以轻松地订阅和发布自定义事件,以及处理浏览器原生事件。

jQuery 提供了一种简单而强大的方式来实现发布-订阅模式,使得在应用中不同模块之间的通信更加灵活和可维护。

在现代的前端开发中,虽然现代框架提供了更先进的状态管理和组件通信机制,但对于一些小型项目或不需要引入复杂状态管理的场景,jQuery 事件系统仍然是一个实用的工具。

  1. 事件绑定:
// 订阅自定义事件
$(document).on('customEvent', function(event, data) {
  console.log('Custom event received with data:', data);
});

// 发布自定义事件
$(document).trigger('customEvent', 'Hello, jQuery!');
  1. 事件命名空间:
// 订阅带有命名空间的事件
$(document).on('customEvent.myNamespace', function(event, data) {
  console.log('Custom event with namespace received with data:', data);
});

// 发布带有命名空间的事件
$(document).trigger('customEvent.myNamespace', 'Hello, jQuery with namespace!');
  1. 一次性事件处理:
// 订阅一次性事件
$(document).one('customEvent', function(event, data) {
  console.log('One-time custom event received with data:', data);
});

// 发布一次性事件
$(document).trigger('customEvent', 'Hello, jQuery one-time!');
  1. 解除事件绑定:
// 解除事件绑定
$(document).off('customEvent');

// 将不再接收 customEvent 事件
$(document).trigger('customEvent', 'This will not be logged.');

最后

发布-订阅模式在前端开发中有许多实际应用场景,它提供了一种松散耦合的机制,允许不同模块或组件之间进行灵活的通信。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码