结合观察者模式和Vue,来学学发布订阅模式

文章目录

前言

一、引入——观察者模式

1.观察者模式的介绍

2.使用观察者模式的场景:

3.举例展示观察者模式的实现

4.对观察者模式的理解

二、发布订阅模式

1.发布订阅模式的介绍

2.发布订阅模式的优、缺点

优点:

缺点:

3.使用发布订阅模式的场景

4.发布订阅模式的实现举例

三、结合Vue框架进行分析

四、小结


前言

        在学习发布订阅模式的时候,我在网上的教程中看到关于观察者模式和发布订阅模式的对比,二者同属于行为型模式。虽然它们在行为上存在相似性,但却是两种不同的设计模式。

        再加上我自己在复习学校OOAD课程上讲过的GoF前面几种模式时,参考了菜鸟网(www.runoob.com)上关于设计模式的教程,看到里面有提到“观察者模式”(但是没有发布订阅模式),于是便好奇点进去看了一下,顺带学习了一下观察者模式。

        后面,在看部分前端Vue面经的时候,了解到Vue框架就是基于发布订阅模式设计的。为了帮助自己理解发布订阅模式的思想以及应用于实际开发中,于是我便对Vue框架的源码进行了分析和学习,再次领悟其中的“发布订阅”思想。


一、引入——观察者模式

        以下内容整理自菜鸟网(www.runoob.com)上的教程关于“观察者模式”的教程。

1.观察者模式的介绍

  • 意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  • 主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
  • 何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知
  • 如何解决:通过面向对象技术弱化这种依赖关系。
  • 关键代码:在抽象类里有一个 ArrayList 存放观察者们。
  • 在实际场景中的应用举例:在拍卖会中,拍卖师观察最高标价,然后通知给其他竞价者竞价。

2.使用观察者模式的场景:

  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。即达到广播效果。
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制

3.举例展示观察者模式的实现

        前面列举的干货都是通过提炼出来的,可能不太便于理解,那就通过编写一个小小的java程序demo来举例帮助理解吧~

        本例子中,创建了Subject 类、Observer 抽象类和扩展了抽象类 Observer 的实体类(BinaryObserver、OctalObserver、HexaObserver)来实现观察者模式。使用ObserverPatternDemo演示类来使用 Subject 和实体类对象来演示观察者模式。

(1)UML类图 、顺序图展示如下:      

关键类图

顺序图

(2) 代码实现:

步骤 1

        创建 Subject 类。

import java.util.ArrayList;
import java.util.List;
 
public class Subject {

    //定义一个用于存放观察者的数组
   private List<Observer> observers 
      = new ArrayList<Observer>();
   private int state;
 
   public int getState() {
      return state;
   }
 
   public void setState(int state) {
      this.state = state;
      notifyAllObservers();
   }
 
   public void attach(Observer observer){
      observers.add(observer);      
   }
 
   public void notifyAllObservers(){
      for (Observer observer : observers) {
         observer.update();
      }
   }  
}

        

步骤 2

        创建 抽象Observer 类。

//Observer:抽象类
public abstract class Observer {
   protected Subject subject;
   public abstract void update();
}

       

步骤 3

        创建实体观察者类(BinaryObserver、OctalObserver、HexaObserver),重写update方法。

//BinaryObserver实体类,用于将十进制转化为二进制
public class BinaryObserver extends Observer{
 
   public BinaryObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
   }
 
   @Override
   public void update() {
      System.out.println( "Binary String: " 
      + Integer.toBinaryString( subject.getState() ) ); 
   }
}
//OctalObserver实体类,用于将十进制转化为八进制
public class OctalObserver extends Observer{
 
   public OctalObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
   }
 
   @Override
   public void update() {
     System.out.println( "Octal String: " 
     + Integer.toOctalString( subject.getState() ) ); 
   }
}
//HexaObserver实体类,用于将十进制转化为十六进制
public class HexaObserver extends Observer{
 
   public HexaObserver(Subject subject){
      this.subject = subject;
      this.subject.attach(this);
   }
 
   @Override
   public void update() {
      System.out.println( "Hex String: " 
      + Integer.toHexString( subject.getState() ).toUpperCase() ); 
   }
}

步骤 4

        在ObserverPatternDemo演示类中使用 Subject 和实体观察者对象。

public class ObserverPatternDemo {
   public static void main(String[] args) {
      Subject subject = new Subject();
 
      new HexaObserver(subject);
      new OctalObserver(subject);
      new BinaryObserver(subject);
 
      System.out.println("First state change: 15");   
      subject.setState(15);
      System.out.println("Second state change: 10");  
      subject.setState(10);
   }
}

步骤 5

        执行程序,输出结果:

4.对观察者模式的理解

        观察者模式适用于当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据的情况。

观察者模式包含以下三种角色:

        Subject(主题/具体主题):可以是接口,后续再由实现类来实现该主题接口;

        也可以直接定义为实现类,表示具体的主题。

        Observer(观察者):接口,规定了具体观察者更新数据的方法。

        ConcreteObserver(具体观察者):实现观察者接口类的实例。

        当Subject(具体主题)内的数据发生改变时,由于Subject内存在存放Observer(观察者)的集合,比如ArrayList,因此Subject内数据变化时就能及时地通知到Observer,Observer对应的ConcreteObserver(具体观察者)就能对数据变化作出不同的响应。实现一个通知,触发多个事件的效果。

  • 观察者模式的优点:

    (1)具体主题和具体观察者间是松耦合关系。因为主题只依赖观察者接口、具体观察者泛化了观察者接口,因此具体主题和具体观察者间不需要互相知道类的信息。

    (2)满足“开闭原则”。主题接口/具体主题仅依赖观察者接口,若要新增观察者接口,不需要修改主题。

  • 缺点:

    (1)若一个被观察者有许多直接、间接观察者,则需要花费大量时间让所有观察者接收到通知。

    (2)观察者模式没有相应机制让观察者知道观察的目标如何发生变化,仅知道观察目标发生了变化。


二、发布订阅模式

        观察者模式中被观察者和观察者之间实现的是松耦合,那么有没有完全解耦的类似模式呢?答案是有的,而发表订阅模式就实现了发布者和订阅者之间的完全解耦

        观察者模式存在一些弊端,比如说观察者模式提供给关联对象的是一种同步通信的手段,就会存在需要花时间等待传输完成的问题。而发布订阅模式大多数时候采用的是使用消息队列(中间件)实现异步并发。

        下面来对发布订阅模式进行介绍。

1.发布订阅模式的介绍

        发布订阅模式与观察者模式的定义其实差不多,

唯一的不同点就在于:

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

        通俗地来说,就是在发布订阅模式中,发布方向订阅方广播发送信息是通过中间件来实现的;而在观察者模式中,被观察者与观察者之间是直接广播信息的。

发布订阅模式在实际场景中的举例:

        比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

        在此场景中,公众号属于发布者(Publisher),用户属于订阅者(Subscriber);用户将订阅公众号的事件注册到调度中心(Channel),公众号作为发布者,当有新文章(Message)发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户新文章发布。

2.发布订阅模式的优、缺点

优点:

(1)松耦合/Independence:

        可以将众多需要通信的子系统(Subsystem)解耦,每个子系统都可以独立管理。而且即使部分子系统下线了,也不会影响系统消息的整体管理。

        同时,为应用程序提供了关注点分离。每个应用程序都可以专注于其核心功能,而消息传递基础结构负责将消息路由到每个消费者手里。

(2)高伸缩性/Scalability:

        发送方向中间件发送信息后就能返回其核心处理业务,不用关注何时信息全部成功发送给订阅方;由中间件负责把信息传递到每一个订阅方手中。

(3)高可靠性/Reliability:

        异步传输的功劳,有助于应用程序在增加的负载下继续平稳运行,并且可以更有效地处理间歇性故障。

(4)灵活性/Flexibility:

        不用关心组件的组合原理,只需他们遵守同一份协议即可。

(5)可测试性/Testability:

        中间件可以被监视,从而降低了测试难度;发送的消息可以作为整体集成测试策略的一部分而被检查或记录。

缺点:

        由于增加了中间件的代理,为系统增加了复杂度,发布方不能实时指导自己的发表的信息是否已被订阅方收到,增加了系统的不确定性。

3.使用发布订阅模式的场景

(1)应用程序需要向大量消费者广播信息,例如微信订阅号

(2)应用程序需要与一个或多个独立开发的应用程序或服务通信,这些应用程序或服务可能使用不同的平台、编程语言和通信协议。

        例如:现在很多的互联网产品同时拥有网页端、pc端、移动端app、小程序等多种形态,当程序员对产品中的某个功能进行了修复,那么应该各个平台的bug都要进行修复,为了避免重复劳动就可以使用发布订阅模式,通过中间件将修改广播到各个view层。

(3)应用程序可以向消费者发送信息,而不需要消费者的实时响应。或者,消费者可能具有与发送者不同的可用性要求或正常运行时间计划。例如你消息在上午发布了出去,消费者计划在下午才去处理这些消息。

(4)被集成的系统被设计为支持其数据的最终一致性模型。

4.发布订阅模式的实现举例

关键部分类图如下:

关键部分顺序图如下:

步骤 1

        定义发布者接口:所有的发布者都要实现该接口,该接口定义添加订阅者、发送消息、移除订阅者方法。

public interface IPublisher {

    /**
     * 添加订阅者
     */
    void emit(ISubscriber subscriber);

    /**
     * 触发消息
     */
    void on(String msg);

    /**
     * 移除订阅者
     */
    void remove(ISubscriber subscriber);

}

步骤 2

        创建发布者实现类:这里以EventBus为例,实现发布者接口,并在该类中创建一个集合来添加存储订阅者。当发布消息的时候遍历该集合,调用订阅者的通知函数,将消息发送出去。

public class EventBus implements IPublisher {

    // 定义一个集合来存储订阅者
    private ArrayList events = new ArrayList<Subscriber>();

    // 发布者名称
    private String name;

    // 创建发布者
    public EventBus(String name) {
        this.name = name;
    }

    @Override
    public void emit(ISubscriber subscriber) {
        // 将每一个新的订阅者添加到集合中维护
        events.add(subscriber);
    }


    @Override
    public void on(String msg) {
        // 触发订阅的原理就是遍历该集合,然后将消息发送给集合中的每一个订阅者
        for (int i = 0; i < events.size(); i++) {
            Subscriber subscriber = (Subscriber) events.get(i);
            subscriber.on(this.name, msg);
        }
    }

    @Override
    public void remove(ISubscriber subscriber) {
        // 移除订阅者
        events.remove(subscriber);
    }

}

步骤 3

        定义订阅者接口:接口定义收到消息监听方法,所有的订阅者都实现该接口。

public interface ISubscriber {
    
    void on(String publisher, String msg);
    
}

步骤 4

        创建订阅者实现类:实现订阅者接口。

public class Subscriber implements ISubscriber {

    // 订阅者名称
    private String name;
    private Receiver receiver;

    public interface Receiver {
        void onMessage(String publisher, String msg);
    }

    // 创建订阅者
    public Subscriber(String name) {
        this.name = name;
    }

    // 创建订阅者,也可以加个回调函数,用来将消息回调出去
    public Subscriber(String name, Receiver receiver) {
        this.name = name;
        this.receiver = receiver;
    }

    @Override
    public void on(String publisher, String msg) {
        // 这里会收到发布者消息,然后做相应的处理,一般这里会创建一个回调函数,通过回调函数将消息发送出去
        System.out.println(publisher + "发布了新消息;" + this.name + ",收到消息通知:" + msg);
        if (receiver != null) {
            receiver.onMessage(publisher, msg);
        }
    }
}

步骤 5

        使用示例:

Subscriber subscriber1 = new Subscriber("张三", new Subscriber.Receiver() {
    @Override
    public void onMessage(String publisher, String msg) {
        // 收到消息
        System.out.println(publisher + ":" + msg);
    }
}); // 创建订阅者张三
Subscriber subscriber2 = new Subscriber("李四"); // 创建订阅者李四
Subscriber subscriber3 = new Subscriber("王五"); // 创建订阅者王五

EventBus eventBus = new EventBus("小明"); // 创建发布者小明
eventBus.emit(subscriber1); // 添加张三订阅者
eventBus.emit(subscriber2); // 添加李四订阅者
eventBus.emit(subscriber3); // 添加王五订阅者

eventBus.on("祝你虎年大吉大利"); // 发布新消息

示例结果: 

小明发布了新消息;张三,收到消息通知:祝你虎年大吉大利
小明:祝你虎年大吉大利
小明发布了新消息;李四,收到消息通知:祝你虎年大吉大利
小明发布了新消息;王五,收到消息通知:祝你虎年大吉大利


三、结合Vue框架进行分析

        在学习发布订阅模式的相关思想的时候,我就联想到vue中的父子组件通信和双向数据绑定,好奇它在实现的时候是不是也应用到了“发布订阅”的模式。因此,我就去网上搜了vue框架的源码以及别人研究vue源码的教程来学习了一下。

        Vue项目目录下的核心文件“src”下的文件目录如下:

├─ src                         // 主要源码所在位置,核心内容
│   ├─ compiler                // 模板编译相关文件,将 template 编译为 render 函数
│       ├─ codegen             // 把AST(抽象语法树)转换为Render函数
│       ├─ directives          // 生成Render函数之前需要处理的东西
│       ├─ parser              // 模板编译成AST
│   ├─ core                    // Vue核心代码,包括了内置组件、全局API封装、Vue实例化、响应式原理、vdom(虚拟DOM)、工具函数等等。
│       ├─ components          // 组件相关属性,包含抽象出来的通用组件 如:Keep-Alive
│       ├─ global-api          // Vue全局API,如Vue.use(),Vue.nextTick(),Vue.config()等,包含给Vue构造函数挂载全局方法(静态方法)或属性的代码。 链接:https://012-cn.vuejs.org/api/global-api.html
│       ├─ instance            // 实例化相关内容,生命周期、事件等,包含Vue构造函数设计相关的代码
│       ├─ observer            // 响应式核心目录,双向数据绑定相关文件。包含数据观测的核心代码
│       ├─ util                // 工具方法
│       └─ vdom                // 虚拟DOM相关的代码,包含虚拟DOM创建(creation)和打补丁(patching)的代码
│   ├─ platforms               // vue.js和平台构建有关的内容 不同平台的不同构建的入口文件也在这里 (Vue.js 是一个跨平台的MVVM框架)
│       ├── web                // web端 (渲染,编译,运行时等,包括部分服务端渲染)
│       │   ├── compiler       // web端编译相关代码,用来编译模版成render函数basic.js
│       │   ├── entry-compiler.js               // vue-template-compiler 包的入口文件
│       │   ├── entry-runtime-with-compiler.js  // 独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
│       │   ├── entry-runtime.js                // 运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。
│       │   ├── entry-server-basic-renderer.js  // 输出 packages/vue-server-renderer/basic.js 文件
│       │   ├── entry-server-renderer.js        // vue-server-renderer 包的入口文件
│       │   ├── runtime        // web端运行时相关代码,用于创建Vue实例等
│       │   ├── server         // 服务端渲染(ssr)
│       │   └── util           // 工具类相关内容
│       └─ weex                // 混合运用 weex框架 (一端开发,三端运行: Android、iOS 和 Web 应用) 2016年9月3日~4日 尤雨溪正式宣布以技术顾问的身份加盟阿里巴巴Weex团队, 做Vue和Weex的整合 让Vue的语法能跨三端
│   ├─ server                  // 服务端渲染相关内容(ssr)
│   ├─ sfc                     // 转换单文件组件(*.vue)
│   └─ shared                  // 共享代码 全局共享的方法和常量

        看到“observer”的时候,我就猜想这个模块是不是就是实现了“发布订阅模式”的呢?自己分析了下源码,然后去看了下教程:

observer模块共分为这几个部分:

  • Observer: 数据的观察者,让数据对象的读写操作都处于自己的监管之下
  • Watcher: 数据的订阅者,数据的变化会通知到Watcher,然后由Watcher进行相应的操作,例如更新视图
  • Dep: Observer与Watcher的纽带,当数据变化时,会被Observer观察到,然后由Dep通知到Watcher。

示意图如下:

        这样看来,其中的Dep不是正与“发布订阅模式”中的中间件所起的作用一样吗?Observer相当于发布方,Watcher相当于订阅方。因此,我认为observer模块实现了“发布订阅模式”。

        

源码分析

 (1)Observer

        Observer类定义在src/core/observer/index.js中,以下是Observer的构造函数:

constructor (value: any) {
  //value:需要被观察的数据对象
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  //给value加上__ob__属性,作为数据已被Observer观察的标志
  def(value, '__ob__', this)
  //判断value是否为数组
  if (Array.isArray(value)) {
    const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    //value为数组:用observeArray遍历value,对value中每一个元素调用observer进行观察
    this.observeArray(value)
  } 
  //value不是数组:walk遍历value上每个key,每个key的数据调用defineReactive来获得该key的set/get控制权
  else {
    this.walk(value)
  }
}

(2)Dep

        Dep是Observer与Watcher之间的纽带,Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。


// Dep是订阅者Watcher对应的数据依赖
var Dep = function Dep () {
  //每个Dep都有唯一的ID
  this.id = uid++;
  //subs用于存放依赖
  this.subs = [];
};

//向subs数组添加依赖
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//移除依赖
Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
//设置某个Watcher的依赖
//这里添加了Dep.target是否存在的判断,目的是判断是不是Watcher的构造函数调用
//也就是说判断他是Watcher的this.get调用的,而不是普通调用
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  //通知所有绑定 Watcher。调用watcher的update()
  for (var i = 0, l = subs.length; i &lt; l; i++) {
    subs[i].update();
  }
};

此外,Dep中还提供了几个接口:

  • addSub: 接收的参数为Watcher实例,并把Watcher实例存入记录依赖的数组中
  • removeSub: 与addSub对应,作用是将Watcher实例从记录依赖的数组中移除
  • depend: Dep.target上存放这当前需要操作的Watcher实例,调用depend会调用该Watcher实例的addDep方法,addDep的功能可以看下面对Watcher的介绍
  • notify: 通知依赖数组中所有的watcher进行更新操作

(3)Watcher

        Watcher是用来订阅数据的变化的并执行相应操作(例如更新视图)的。以下是Watcher的构造器函数:

constructor (vm, expOrFn, cb, options) {
  //vm:组件实例
  this.vm = vm
  vm._watchers.push(this)
  // options
  if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
  } else {
    this.deep = this.user = this.lazy = this.sync = false
  }
  //cb:watcher运行后的回调函数
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  this.deps = []
  this.newDeps = []
  this.depIds = new Set()
  this.newDepIds = new Set()
  //expOrFn:要订阅的数据字段;或是一个要执行的函数
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
      this.getter = function () {}
      process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      )
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get()
}

watcher实例上有这些方法:

  • get: 将Dep.target设置为当前watcher实例,在内部调用this.getter,如果此时某个被Observer观察的数据对象被取值了,那么当前watcher实例将会自动订阅数据对象的Dep实例
  • addDep: 接收参数dep(Dep实例),让当前watcher订阅dep
  • cleanupDeps: 清除newDepIds和newDep上记录的对dep的订阅信息
  • update: 立刻运行watcher或者将watcher加入队列中等待统一flush
  • run: 运行watcher,调用this.get()求值,然后触发回调
  • evaluate: 调用this.get()求值
  • depend: 遍历this.deps,让当前watcher实例订阅所有dep
  • teardown: 去除当前watcher实例所有的订阅

四、小结

        以上就是本人对发布订阅模式的一篇学习笔记,本人认为,发布订阅模式主要用于解决了这两个问题:

        发布方如何将新信息推送给所有订阅方;发布方和订阅方的完全解耦的异步并发。

        订阅方不用再去频繁地查看发布方到底发了新信息没有;而发布方将新的信息传送到中间件,让中间件帮助完成广播给所有订阅方的工作,可见发布方和订阅方之间没有直接的关联,实现了解耦。因此发布方可以更加专注于自己的本身业务,中间件也可以对广播的时间进行自己的调整,体现了异步的思想。

        

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值