[js话设计模式]订阅发布模式和观察者模式


前言

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

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

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

——《Head First设计模式》

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

[js话设计模式]订阅发布模式和观察者模式
img
[js话设计模式]订阅发布模式和观察者模式
img

(书中部分截图)

…….

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

js实现的大概思路:

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


参考资料

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

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

《Head First设计模式》





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

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

// -----------定义发布者----------------------------
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(thisarguments);
    }
};
// -----------定义发布者 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");
[js话设计模式]订阅发布模式和观察者模式
img

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

// ---------------定义发布者----------------------------
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");

[js话设计模式]订阅发布模式和观察者模式
img

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

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

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

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

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

// ---------------定义发布者----------------------------
let publisher = {
    list: [], // 缓存列表 存放订阅者回调函数
    addListenerfunction (targetType, func{
        if (!this.list[targetType]) {
            // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
            this.list[targetType] = [];
        }
        this.list[targetType].push(func);
    },
    publishfunction ({
        /*
          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");
[js话设计模式]订阅发布模式和观察者模式
img

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

如果要实现退订功能呢?

removeListener()的实现思路:

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

// ---------------定义发布者----------------------------
let publisher = {
    list: [], // 缓存列表 存放订阅者回调函数
    addListenerfunction (targetType, func{
        if (!this.list[targetType]) {
            // 如果没有用户订阅过该文章类型的频道,则给该频道创建一个缓存列表
            this.list[targetType] = [];
        }
        this.list[targetType].push(func);
    },
    publishfunction ({
        /*
          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");
[js话设计模式]订阅发布模式和观察者模式
img

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

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

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");

[js话设计模式]订阅发布模式和观察者模式
img





结语

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

原文始发于微信公众号(豆子前端):[js话设计模式]订阅发布模式和观察者模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/57025.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!