• JavaScript 设计模式:发布订阅


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

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

    ①发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅ajax请求的error、succ等事件。

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

     买房者收到售楼处发布的房源售卖通知。售楼处就是发布者,买房者就是订阅者,订阅的是售卖房源的信息。买房者有很多人,他们会订阅不同的房源(不同的地理位置,价格、周边等),发布者可以根据不同的订阅者发布不同的信息。

    1. var salesOffices = {}; //发布者售楼处
    2. salesOffices.clientList = {}; // 一个客户列表
    3. salesOffices.listen = function ( key, fn ) { // 一个收集用户订阅信息的方法
    4. if ( ! this.clientList[ key ] ) {
    5. this.clientList[ key ] = [];
    6. }
    7. this.clientList[ key ].push( fn );
    8. };
    9. salesOffices.trigger = function() { // 一个发布订阅消息给用户的方法
    10. var key = Array.prototype.shift.call( arguments );
    11. var fns = this.clientList[ key ];
    12. if ( ! fns || fns.length === 0 ) {
    13. return false;
    14. }
    15. for ( var i=0, fn; fn = fns[i++]; ) {
    16. fn.apply( this, arguments );
    17. }
    18. };
    19. ***********************************************************************
    20. var clientA = { // A订阅的信息是88平米的价格
    21. key: 'squareMeter88',
    22. fn: function( price ){
    23. console.log('The squareMeter of 88 is :' + price +' RMB.')
    24. }
    25. };
    26. var clientB = { // B订阅的信息是110平米的价格
    27. key: 'squareMeter110',
    28. fn: function( price ){
    29. console.log('The squareMeter of 110 is :' + price +' RMB.')
    30. }
    31. };
    32. //收到用户的订阅
    33. salesOffices.listen( clientA.key, clientA.fn );
    34. salesOffices.listen( clientB.key, clientB.fn );
    35. //售楼开始发布房源
    36. salesOffices.trigger('squareMeter88', '1800000' );
    37. salesOffices.trigger('squareMeter110', '2500000' );

     我们可以把发布订阅的功能提取为一个对象,再定义一个 安装函数,这样其他售楼处也可以安装这个功能。给 发布订阅对象 PandS 添加一个 remove 方法,取消订阅的事件。

    1. // 发布订阅对象
    2. var PandS = {
    3. clientList: [], // 一个客户列表
    4. listen: function( key, fn ) {……}, // 一个收集用户订阅信息的方法
    5. trigger: function() {……}, // 一个发布订阅消息给用户的方法
    6. remove: function( key, fn ) { // 增加一个删除订阅事件的方法
    7. var fns = this.clientList[ key ];
    8. if ( ! fns ) { return false; } // 如果没有被订阅,则直接返回
    9. if ( ! fn ) { // 如果没有传 fn, 则表示 要取消 key 所有的订阅事件
    10. fns && ( fns.length = 0 );
    11. } else {
    12. for (var len = fns.length -1; len >= 0; len-- ) {
    13. if ( fns[ len ] === fn ) {
    14. fns.splice( len, 1 );
    15. }
    16. }
    17. }
    18. }
    19. };
    20. // 安装函数
    21. var installPandS = function( obj ) {
    22. for( var i in PandS ) {
    23. obj[ i ] = PandS[ i ]; // 复制对象的属性和方法,即安装
    24. }
    25. };
    26. //测试:售楼处REUB 安装 发布订阅功能
    27. var REUB = {};
    28. installPandS( REUB ); // 安装好了
    29. REUB.listen( clientA.key, clientA.fn );
    30. REUB.trigger('squareMeter88', '1800000' );
    31. REUB.remove( clientA.key, clientA.fn ); // 删除A的订阅

    网站登录 实例:

    1. var login = { // 一个叫 login 的发布者
    2. clientList: [],
    3. listen: function( client ) {……},
    4. trigger: function() {……},
    5. remove: function( client ) {……}
    6. };
    7. $.ajax('http://xxx.com?login', function(data) {
    8. login.trigger('loginSuccess', data); // 发布登陆成功消息
    9. });
    10. // header模块订阅
    11. var header = (function(){
    12. // 用户订阅登录成功信息
    13. login.listen( {
    14. key: 'loginSuccess',
    15. fn: function( data) {
    16. header.setAvatar( data.avatar );
    17. }
    18. } );
    19. return {
    20. setAvatar: function( data ) {
    21. console.log('设置header 头像: ', data );
    22. }
    23. }
    24. })();
    25. // nav 模块订阅 略

     总结:我们随时可以把setAvatar的方法名改成setTouxiang。如果有一天在登录完成之后,又增加一个刷新收货地址列表的行为,那么只要在收货地址模块里加上监听消息的方法即可,而这可以让开发该模块的同事自己完成,你作为登录模块的开发者,永远不用再关心这些行为。

    先发布后订阅 与 命名冲突

     前面的代码都是只能 先订阅某个事件,发布必须在其后,否则无法获取到真正的订阅。为了解决这个问题,可以使用一个存放离线事件的堆栈,当发布事件的时候,将动作包裹在函数里,将函数存入堆栈中。等到终于有对象来订阅此事件的时候,我们将遍历堆栈并且依次执行这些包装函数,也就是重新发布里面的事件。

     将 发布订阅对象独立出来,取名为 Event。里面只有 clientList 来存放消息名和回调函数,为了避免冲突,给 Event对象提供创建命名空间的功能。

    1. // 先发布后订阅 效果
    2. Event.trigger( 'click', '123' );
    3. Event.listen( 'click', function(a) { console.log(a); } ); // 输出 '123'
    4. // 命名空间效果
    5. Event.create('name1').listen( 'click', function(a) { console.log(a); } ); // 输出 '123'
    6. Event.create('name1').trigger( 'click', '123' );
    7. Event.create('name2').listen( 'click', function(a) { console.log(a); } ); // 输出 '456'
    8. Event.create('name2').trigger( 'click', '456' );

    实现 略。(详细参见:JavaScript设计模式与开发实践 8.11节)

     发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可以用来帮助实现一些别的设计模式,比如中介者模式。从架构上来看,无论是MVC还是MVVM,都少不了发布—订阅模式的参与,而且JavaScript本身也是一门基于事件驱动的语言。

     创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

    手写一个发布订阅:

    1. // 发布订阅中心, on-订阅, off取消订阅, emit发布, 内部需要一个单独事件中心caches进行存储;
    2. interface CacheProps {
    3. [key: string]: Array<((data?: unknown) => void)>;
    4. }
    5. class Observer {
    6. private caches: CacheProps = {}; // 事件中心
    7. on (eventName: string, fn: (data?: unknown) => void){ // eventName事件名-独一无二, fn订阅后执行的自定义行为
    8. this.caches[eventName] = this.caches[eventName] || [];
    9. this.caches[eventName].push(fn);
    10. }
    11. emit (eventName: string, data?: unknown) { // 发布 => 将订阅的事件进行统一执行
    12. if (this.caches[eventName]) {
    13. this.caches[eventName].forEach((fn: (data?: unknown) => void) => fn(data));
    14. }
    15. }
    16. off (eventName: string, fn?: (data?: unknown) => void) { // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
    17. if (this.caches[eventName]) {
    18. const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : [];
    19. this.caches[eventName] = newCaches;
    20. }
    21. }
    22. }
  • 相关阅读:
    HarmonyOS DevEso环境搭建
    webpack快速入门-处理样式资源
    php沿河农产品特卖网站的设计与实现毕业设计源码201524
    Sam Altman的成功学|升维指南
    零基础非计算机专业转行软件测试可行吗?
    Alibaba(获得店铺的所有商品) API 接口
    三个统一..........
    几行 Python 代码就可以提取数百个时间序列特征
    Android—AMS启动
    Python学习:输入与输出教程
  • 原文地址:https://blog.csdn.net/hl199626/article/details/125407329