设计模式之观察者模式——Js实现

news/2024/7/10 1:22:58 标签: js, javascript, vue, 设计模式

文章目录

    • 前言
    • 参考资料
    • 1、版本1 你发布我就收到
    • 2、版本2 我只想收到我关注类型的消息
    • 3、版本3 不仅能发布文章,还能发布手机!
    • 4、版本4 什么玩意?退订退订!
    • 5、版本5 以类之名,用ES6语法重构之!
    • 结语

前言

《Head First设计模式》第二章中介绍的模式为“观察者模式”。书中定义如下。

“观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并自动更新。”

“出版者+订阅者=观察者模式。”

​ ——《Head First设计模式

“观察者模式”有时候又称为订阅发布者模型。在查阅了一些资料之后,我个人感觉这两者的基本框架好像基本都是一样的。只是有的时候,“订阅发布模型”会更强调”发布者“和”订阅者“之间的解耦合,会单独剥离出一个消息调度中心模块,发布者只需关系发布内容,订阅者也只会受到从调度中心发来的自己想要的消息,究竟要不要剥离出这个消息调度中心呢?我个人觉得应具体情况具体分析,工程量小的时候,这种“解耦合”往往是没必要的。

img

img

(书中部分截图)

接下来,我将以js代码的方式来实现这个模型。

js实现的大概思路:

  1. 定义发布者{发布方法,添加订阅者方法,退订方法,消息调度结构}。
  2. 给发布者设置用于存放回调函数来通知订阅者的缓存列表(消息调度结构)。
  3. 最后就是发布消息,发布者遍历这个缓存列表,依次触发里面存放的订阅者回调函数。


参考资料

剖析Vue原理&实现双向绑定MVVM

https://www.cnblogs.com/tugenhua0707/p/4687947.html Javascript中理解发布–订阅模式

《Head First设计模式




1、版本1 你发布我就收到

现在,publisher是一个出版社,user订阅了这个出版社,只要publisher发表任何新作品,用户都能收到。

js">// -----------定义发布者----------------------------
let publisher = {};
publisher.list = [];   // 缓存列表 存放订阅者回调函数

// 增加订阅者
publisher.addListener = function (func) {
    this.list.push(func);
};

// 发布消息
publisher.publish = function () {
    // 遍历订阅回调函数队列(告诉每个订阅者,有新消息了!)
    for (let i = 0, func; func = this.list[i]; i++) {
        func.apply(this, arguments);
    }
};
// -----------定义发布者 End----------------------------


// 以文章发布为例

// UserA的订阅后接受推送的行为
let userASubscribe = function (author, article) {
    console.log("----------UserA收到新消息!--------------");
    console.log("作者:" + author);
    console.log("文章:" + article);
};

// UserB的订阅后接受推送的行为
let userBSubscribe = function (author, article) {
    console.log("----------UserB收到新消息!--------------");
    console.log("作者:" + author);
    console.log("文章:" + article);
};

// 订阅
publisher.addListener(userASubscribe);
publisher.addListener(userBSubscribe);

// 模拟文章发布
publisher.publish("作者1", "文章1");
publisher.publish("作者2", "文章2");

img



2、版本2 我只想收到我关注类型的消息

js">// ---------------定义发布者----------------------------
let publisher = {};
publisher.list = []; // 缓存列表 存放订阅者回调函数

// 增加订阅者
publisher.addListener = function (target, func) {
    if (!this.list[target]) {
        // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
        this.list[target] = [];
    }
    this.list[target].push(func);
};

// 发布消息
publisher.publish = function () {
    /*
        shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
        如果数组为空则返回undefined。此方法会改变数组自身。
        例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
        */
    // console.log(arguments);

    let [..._arguments] = arguments;  // 注意:这里得用深拷贝方法
    let targetType = Array.prototype.shift.call(arguments); 

    // console.log(arguments);
    let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合

    console.log("publisher发布了" + targetType + "类型的文章")
    // 如果没有订阅过该文章类型频道的话,则返回
    if (!funcs || funcs.length === 0) {
        return;
    }

    // 遍历,执行每个订阅了该消息的订阅者回调函数
    for (let i = 0, func; func = funcs[i]; i++ ) {
        func.apply(this, _arguments); // arguments 是发布消息时附送的参数
    }
};

// -------------------定义发布者 End----------------------------
// ---------以文章发布为例,publish可以看做是一个文章出版社---------------

// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
    console.log("----------UserA收到新消息!--------------");
    console.log("文章类型:" + articleType);
    console.log("文章名:" + articleName);
    // console.log("----------UserA收到新消息结束!--------------");
};

// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
    console.log("----------UserB收到新消息!--------------");
    console.log("文章类型:" + articleType);
    console.log("文章名:" + articleName);
    //console.log("----------UserB收到新消息结束!--------------");
};

// 订阅,UserA订阅了新闻频道,UserB订阅了娱乐频道 
publisher.addListener("新闻", userASubscribe);
publisher.addListener("娱乐", userBSubscribe);

// 模拟文章发布
publisher.publish("新闻", "文章1");
publisher.publish("娱乐", "文章2");
publisher.publish("游戏", "文章3");

img

如此一来,UserA只会接收到"新闻"类型的文章,UserB则只接收到娱乐类型的文章。

同时可以看到,publisher发出文章的同时,就会立马触发推送函数通知订阅者。之后发布的等前面发布的通知完成后才会执行发布动作。



3、版本3 不仅能发布文章,还能发布手机!

如果有其他类型的发布者也要使用呢?

例如,除了文章发布,还可以使用在手机发布、商品发布等情况,那么可以定义一个initPublisher()方法来为我们具体的发布者添加list,addListener,publish等属性和方法。从类的角度看,就相当于定义一个基类,后面详细的发布至只要从这里继承就行了。

js">// ---------------定义发布者----------------------------
let publisher = {
    list: [], // 缓存列表 存放订阅者回调函数
    addListener: function (targetType, func) {
        if (!this.list[targetType]) {
            // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
            this.list[targetType] = [];
        }
        this.list[targetType].push(func);
    },
    publish: function () {
        /*
          shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
          如果数组为空则返回undefined。此方法会改变数组自身。
          例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
        */
        // console.log(arguments);

        let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
        let targetType = Array.prototype.shift.call(arguments);

        // console.log(arguments);
        let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合

        console.log("publisher发布了" + targetType + "类型的xxx");
        // 如果没有订阅过该文章类型频道的话,则返回
        if (!funcs || funcs.length === 0) {
            return;
        }

        // 遍历,执行每个订阅了该消息的订阅者回调函数
        for (let i = 0, func; (func = funcs[i]); i++) {
            func.apply(this, _arguments); // arguments 是发布消息时附送的参数
        }
    },
};

// 初始化发布者
let initPublisher = function(basePublisher, myPublisher){
    for(let i in basePublisher){
        myPublisher[i] = basePublisher[i];
    }
}
// -------------------定义发布者 End----------------------------

// -------------------以手机发布为例---------------
let phonePublisher = {
    name: "雷军",
    motto: "R U OK?"
};
initPublisher(publisher, phonePublisher)


// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
    console.log("----------UserA收到新消息!--------------");
    console.log("类型:" + articleType);
    console.log("名称:" + articleName);
    console.log("----------UserA收到新消息结束!--------------");
};

// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
    console.log("----------UserB收到新消息!--------------");
    console.log("类型:" + articleType);
    console.log("名称:" + articleName);
    console.log("----------UserB收到新消息结束!--------------");
};

// 订阅,UserA订阅了新闻频道,UserB订阅了娱乐频道
publisher.addListener("小米", userASubscribe);
publisher.addListener("红米", userBSubscribe);
publisher.addListener("黑米", userBSubscribe);


// 雷军开发布会了
publisher.publish("紫米", "红色 紫米12");
publisher.publish("黑米", "红色 黑米12");
publisher.publish("蓝米", "蓝色 蓝米12");
publisher.publish("小米", "黑色至尊小米12");
publisher.publish("小米", "白色简约小米12");
publisher.publish("红米", "黄色红米12");

img



4、版本4 什么玩意?退订退订!

如果要实现退订功能呢?

removeListener()的实现思路:

先找到对应的订阅类型targetType,然后遍历该类型的订阅者回调函数List,找出该用户的订阅回调函数,并从列表中删去(可以使用二分法查找)。

js">// ---------------定义发布者----------------------------
let publisher = {
    list: [], // 缓存列表 存放订阅者回调函数
    addListener: function (targetType, func) {
        if (!this.list[targetType]) {
            // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
            this.list[targetType] = [];
        }
        this.list[targetType].push(func);
    },
    publish: function () {
        /*
          shift()方法从数组中删除第一个元素,并返回该元素的值,此方法会改变数组的长度。
          如果数组为空则返回undefined。此方法会改变数组自身。
          例如: "新闻", "文章1"被传入时,会取走"新闻",此时arguments只剩下"文章1"
        */
        // console.log(arguments);

        let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
        let targetType = Array.prototype.shift.call(arguments);

        // console.log(arguments);
        let funcs = this.list[targetType]; // 取出该消息对应的回调函数的集合

        console.log("publisher发布了" + targetType + "类型的xxx");
        // 如果没有订阅过该文章类型频道的话,则返回
        if (!funcs || funcs.length === 0) {
            return;
        }

        // 遍历,执行每个订阅了该消息的订阅者回调函数
        for (let i = 0, func; (func = funcs[i]); i++) {
            func.apply(this, _arguments); // arguments 是发布消息时附送的参数
        }
    },
};

// 取消订阅
publisher.removeListener = function (targetType, func) {
    let funcs = this.list[targetType];

    // 没有人订阅过
    if (!funcs) {
        return false;
    }

    // 如果没有传入具体的回调函数,表示需要取消对应消息的所有订阅
    // 类似场景:文章、商品等下架
    if (!func) {
        func && (funcs.length = 0);
    } else {
        for (let i = funcs.length - 1; i >= 0; i--) {
            if (funcs[i] === func) {
                // 删除订阅者的回调函数
                funcs.splice(i, 1); 
            }
        }
    }
};

// 初始化发布者
let initPublisher = function (basePublisher, myPublisher) {
    for (let i in basePublisher) {
        myPublisher[i] = basePublisher[i];
    }
};
// -------------------定义发布者 End----------------------------

// ---------------以手机发布为例---------------
let phonePublisher = {
    name: "雷军",
    motto: "R U OK?",
};
initPublisher(publisher, phonePublisher);

// UserA的订阅后接受推送的行为
let userASubscribe = function (articleType, articleName) {
    console.log("----------UserA收到新消息!--------------");
    console.log("类型:" + articleType);
    console.log("名称:" + articleName);
    console.log("----------UserA收到新消息结束!--------------");
};

// UserB的订阅后接受推送的行为
let userBSubscribe = function (articleType, articleName) {
    console.log("----------UserB收到新消息!--------------");
    console.log("类型:" + articleType);
    console.log("名称:" + articleName);
    console.log("----------UserB收到新消息结束!--------------");
};

// 订阅
phonePublisher.addListener("小米", userASubscribe);
phonePublisher.addListener("红米", userBSubscribe);
phonePublisher.addListener("黑米", userBSubscribe);

// 取消订阅,例如:在发布前,UserB突然又不想订阅黑米了
phonePublisher.removeListener("黑米", userBSubscribe)

// 雷军开发布会了
phonePublisher.publish("紫米", "红色 紫米12");
phonePublisher.publish("黑米", "红色 黑米12");
phonePublisher.publish("蓝米", "蓝色 蓝米12");
phonePublisher.publish("小米", "黑色至尊小米12");
phonePublisher.publish("小米", "白色简约小米12");
phonePublisher.publish("红米", "黄色红米12");

img



5、版本5 以类之名,用ES6语法重构之!

接下来,将使用ES6语法,引入类,使得代码更好理解,更加面向对象。

js">class Publisher {
  constructor() {
    this.observersList = [];
  }
  addObserver(targetType, observer) {
    if (!this.observersList[targetType]) {
      // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
      this.observersList[targetType] = [];
    }
    this.observersList[targetType].push(observer);
  }
  // 退订
  removeObserver(targetType, observer) {
    let observers = this.observersList[targetType];

    if (!observers) {
      return false;
    }
    if (!observer) {
      observers.length = 0;
    }

    let index = observers.indexOf(observer);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  }
  publish() {
    let [..._arguments] = arguments; // 注意:这里得用深拷贝方法
    let targetType = Array.prototype.shift.call(arguments);
    let observers = this.observersList[targetType]; // 取出该消息对应的回调函数的集合
    
    console.log(targetType + "类型的消息更新啦!");
    
    if(!observers || observers.length===0){
      return
    }

    observers.forEach(function (observer) {
      observer.receive(targetType);
    });
  }
}

class Observer {
  constructor(name) {
    this.name = name;
  }
  receive(targerType) {
    console.log(this.name + "收到" + targerType + "类型更新的消息!");
  }
}

let phonePublish01 = new Publisher();

phonePublish01.addObserver("小米12", new Observer("订阅者01"));
phonePublish01.addObserver("红米12", new Observer("订阅者02"));

phonePublish01.publish("小米12", "黑色款小米12");
phonePublish01.publish("大米12", "黑色款大米12");
phonePublish01.publish("红米12", "黑色款红米12");
phonePublish01.publish("蓝米12", "黑色款蓝米12");

img




结语

本文大概就先写这么多,接下来我会继续学习研究,写一篇结合obj.setProperty + addEventListener + 观察者模式来实现Vue双向绑定的文章。


http://www.niftyadmin.cn/n/555395.html

相关文章

Python概念(八)字符串格式化:%和.format

https://www.cnblogs.com/nulige/p/6115793.html转载于:https://www.cnblogs.com/Jomini/p/8763153.html

html flex几个属性,CSS Flex布局属性整理

Flex布局display: flex; 将对象作为弹性伸缩盒展示,用于块级元素display: inline-flex; 将对象作为弹性伸缩盒展示,用于行内元素注意兼容问题:webkit内核浏览器应使用前缀-webkitIE浏览器,可以很好的兼容IE11版本,对于…

结对项目

一、Coding.Net项目地址: https://git.coding.net/gemyty/team.git 二、PSP表格估算时间: PSP 任务内容 计划共完成需要的时间(min) Planning 计划 70 Estimate 估计这个任务需要多少时间, 并规划大致工作步骤 70 Develo…

VUE2双向绑定——数据劫持+订阅发布模式

文章目录前言参考资料初级版本实现publisher实现消息订阅中心实现Subscriber实现绑定函数完整代码进阶引入compile,并封装成MVVMES6 class语法版本结语前言 单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时&…

web 服务器

//web 服务器-启动和停止//apacheservice httpd start //启动service httpd restart //重新启动service httpd stop //停止服务sudo /etc/init.d/apache2 startsudo /etc/init.d/apache2 stop//ngixservice nginx start //启动service nginx stop //重新启动service nginx rest…

计算机应用能力培训反思,计算机应用能力提升培训心得.doc

计算机应用能力提升培训心得.doc计算机应用能力提升培训心得我们首先要承认,无论技术变革到怎样的阶段,最基本的,还是 要满足学生需求,最大限度的唤起学生原有的知识经验和潜能。只有 这样,学生的学习潜能,…

Django基本命令

下载Django pip3 install django #默认下载最新版 pip3 install django1.11.1 #手动选择版本 创建Django项目 格式:django-admin startproject 项目名,如: django-admin startproject mysite 创建APP应用 格式&#xff1…

常见浏览器及其内核对应

截止到2020年8月,目前常见浏览器使用的内核如下表所示。 浏览器目前使用的内核IETridentOperaPresto->Webkit->BlinkSafariWebkitFirefoxGeckoChromeBlinkEdgeEdgeHTML(Trident的一个分支) -> ChromiumQQ浏览器Trident(兼容) Chro…