设计模式行为型之观察者模式

实验介绍

本实验为大家介绍的是观察者模式,我们通过家长群的例子为大家仔细地梳理了这一模式的基本概念,帮助大家建立基本认识。虽然为大家介绍这一模式在前端热门框架 VUE 中的真实应用,并且还区分了观察者模式与发布订阅模式,帮助大家理清概念,相信通过本实验的学习,大家一定能对这一模式有一个深刻的认识。

知识点

  • 观察者模式介绍
  • 实现家长群通知功能
  • VUE 双向数据绑定
  • 观察者模式与发布订阅模式
  • 实验挑战

观察者模式介绍

观察者模式就是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

图例:

图片描述

生活中存在着许多这样的例子。例如:智能设备的发展让学校和家长之间的联系变得更加紧密了。同学们最熟悉的就是一个名叫“家长群”的沟通方式,它让家长可以随时的知道自己孩子在学校的情况。

实际上,“家长群”就是一种观察者模式在生活中的应用。当老师建立了一个家长群,并且在群里发布通知的时候,家长们都能得到通知并进行对应的行为。这不就是一种一对多的依赖关系嘛。

在上述图例中,老师代表的就是被观察者,而家长们则代表观察者。

而我们又如何通过代码来实现这样一个场景呢?

实现家长群通知功能

本小节我们就会通过代码实现 “ 家长群 ” 功能。

首先,在观察者模式的关系中,要实现 “ 功能 ”,自然就需要一个支持 “ 被观察的 ” 对象,也就是在这个例子中的老师。

因此,我们首先来实现一个老师类:

新建一个 index.html 文件,及 observer.js 文件,并引入。

// observer.js

/**
 * 定义被观察者 - 老师
 */
class Teacher {
  constructor() {
    this.patriarchs = []; // 家长们(观察者), patriarch: 家长
  }
}

在上面的 Teacher 类的构造函数中,我们申明了一个 patriarchs 变量,它是一个数组,用于存放观察者们(家长)。

那么有了被观察者后,自然就应该有观察者了,因此,接下来我们就需要实现一个家长类:

// observer.js

/**
 * 观察者 - 家长
 */
class Patriarch {
  constructor(name) {
    this.name = name;
  }
}

此时的家长类中仅仅存在一个 name 属性。现在我们就实现了观察者模式中最重要的两部分:被观察者与观察者。

但是细心的同学可能已经发现,上面两个类的存在好像没有什么意义,因为除了固有属性以外没法做任何操作,也自然不可能实现一种联动关系了。

没错,下面我们就要逐步为他们增添实际功能。

第一步,我们需要在老师类中实现添加订阅者的功能,这样才能建立起观察者与被观察者之间的联系。

// observer.js

class Teacher {
  constructor() {
    this.patriarchs = []; // 家长们(观察者), patriarch: 家长
  }

  // 添加观察者
  addPatriarch(patriarch) {
    // 一次只能添加观察者
    this.patriarchs.push(patriarch);
  }
}

第二步,需要为家长们添加一个响应方法,这样老师才能知道家长正确的收到了通知,我们可以这样做:

// observer.js

class Patriarch {
  constructor(name) {
    this.name = name;
  }

  // 家长回复行为,通过打印模拟家长收到通知
  action() {
    console.log(`${this.name}收到老师的通知了!`);
  }
}

完成了以上两步之后,还剩下最重要的一步,那就是实现 “ 通知 ” 能力。我们可以实现一个 notify 方法:

// observer.js

class Teacher {
  constructor() {
    this.patriarchs = []; // 家长们(观察者), patriarch: 家长
  }

  // 添加观察者
  addPatriarch(patriarch) {
    // 一次只能添加观察者
    this.patriarchs.push(patriarch);
  }

  // 通知功能
  notify() {
    this.patriarchs.forEach((patriarch) => {
      // 模拟老师正确通知给家长了
      console.log(`通知给${patriarch.name}了!`);

      // 执行家长行为
      patriarch.action();
    });
  }
}

现在,就完成了观察者与被观察者之间的联系,并且实现了通知功能,下面让我们看看它是不是真的如我们所料实现了正确的观察者模式:

首先我们需要创建被观察者,也就是老师:

// observer.js

// 1、创建被观察者
const teacher = new Teacher();

其次,为被观察者添加所属的观察者,建立二者联系:

// observer.js

// 2、并添加观察者
teacher.addPatriarch(new Patriarch("小明家长"));
teacher.addPatriarch(new Patriarch("张三家长"));
teacher.addPatriarch(new Patriarch("李雷家长"));

最后,执行通知操作,看下是否能正确打印:

// observer.js

// 3、执行通知发布
teacher.notify();

打印结果如下:

通知给小明家长了!
小明家长收到老师的通知了!
通知给张三家长了!
张三家长收到老师的通知了!
通知给李雷家长了!
李雷家长收到老师的通知了!

可以看到,我们在老师发布通知时顺利的通知到了所有的家长。

过程可以用下图表示:

图片描述

相信通过这样一个小例子,大家对观察者模式也有一定的了解。但是这还不够,在下一小节中,我们将学习 vue 中核心的数据双向绑定原理,其本质也是利用了观察者模式。

VUE 双向数据绑定

在 VUE 这门框架中,最独特的特性之一是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。简单来讲:修改数据,视图更新。

注意:本实验中介绍的均为 VUE2 版本

这样一个行为就让我们对数据与视图交互操作大大简化,无须主动的进行相关视图渲染操作。

尽管在使用上非常简单,但是理解其工作原理也非常重要,这样可以帮助你避免一些常见问题。

引用官网上的一张图:

图片描述

在 Vue 中,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 会遍历这个对象的全部属性,随后会使用 Object.defineProperty 把这些属性全部转为 getter/setter。这一点在上图中的紫色圆圈中可以看出。

尽管经过转换后的 getter/setter 对用户是不可见的,但是 Vue 却能对它们进行追踪。当 data 中的某个属性被访问或者是修改(或者说是某个属性对应的 setter 被触发时),则可以通知(Notify)图中的 watcher 。而 watcher 则会使得相关的组件进行重新渲染。

在上面的叙述中,你能感受到应该有一个 “ 监听器 ”,它不仅能对数据的改变实现监听,还能对监听到的数据进行通知转发,而进行转发的这一部分正扮演着被观察者的角色。

自然,watcher 部分就是观察者的扮演者。它们相互之间的关系正是对观察者模式的应用。

我们可以简单的来看一下源码:

Observer 部分:

export class Observer {
  // 每个属性、对象、数组上都有一个 Dep 类型,Dep 类主要就是收集用于渲染的 watcher,
  // 这一点就像我们在前面例子中提到的 patriarchs 变量作用类似
  dep: Dep
  vmCount: number

  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()

    // ...

    // 数组情况
    if (isArray(value)) {
      // ...
      if (!shallow) {

        // 这里实际上就是一个循环处理操作
        this.observeArray(value)
      }
    } else {
      // 对象情况
      // 这里就是前面提到的,将 data 中的属性转换为 getter/setter
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]

        // 转换 getter/setter
        defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
      }
    }
}

/**
 * 尝试为值创建观察者实例,
 * 没有则返回新的观察者,否则直接返回
 */
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {

  // 不是对象直接 return
  if (!isObject(value) || isRef(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void

  // 已经存在观察者,则不做改变
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if ( shouldObserve /* 省略一大堆条件 */) {

    // 不存在既有观察者,则创建一个新的
    ob = new Observer(value, shallow, ssrMockReactivity)
  }
  return ob
}

那么 watcher 部分又是怎样的呢:

/**
 * 解析表达式,收集依赖项; 并在表达式值更改时触发回调。
 */
export default class Watcher implements DepTarget {
    constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {

    // ...

    // 调用 get 方法
    this.value = this.lazy ? undefined : this.get()
  }
}

// 评估getter,并重新收集依赖项。
get() {

    // ...

    //深度访问对象内部每一个值
    if (this.deep) {
    traverse(value)
    }

    //弹出target防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖
    popTarget()

    // 清理依赖项集合
    this.cleanupDeps()
  }

上面仅仅是小部分源码,大体的对 Observerwatcher 这两部分进行了核心概述,在设计模式中限于篇幅,不可能对源码进行详细介绍,因此对 VUE 源码感兴趣的同学可以自行从 GitHub 下载。

当然了,如果你现在还处在前端的初级阶段,或者是对 VUE 还不太了解,那么可以暂时先跳过对 VUE 源码的分析,这一部分并不会影响对观察者模式的理解。之所以介绍这一块内容,是为了让大家看一看观察者模式在前端中的真实应用场景。

监听器 是主动观察?而一开始的家长群是被动观察?

观察者模式与发布订阅模式

通常情况下,你可以简单的认为这两种模式是一个概念,因为在核心思想上这二者确实没有本质的差别。

但是它们之间也确实存在细微的区别。我们现在来简单区分下。

还是以上面的家长群为例,我们把这种通过老师建群,把家长都拉进一个群里从而实现的通知模式称为观察者模式。那么什么情况下是发布订阅模式呢?

实际上,如果老师没有建群,而是在学校的官方网站上发布了相关信息,而家长们需要通过访问这个网站才能获取到老师发布分消息的情况,这就是发布订阅模式。

可能大家看到这里大家会有一点迷糊,我们用精炼的语言来抽取这两种情况:

  1. 观察者模式:不涉及第三方平台,建立被观察者与观察者的之间联系,例如老师在群里直接发消息,家长们直接在群里接收消息。
  2. 发布订阅模式:通过统一的第三方平台,发布者在平台上发布消息,订阅者在平台上查看消息,这种情况下发布者与订阅者之间没有直接的联系。例如上面的学校官网就是一个第三方平台。

总结来说,它们二者之间的区别就在于是否是通过第三方平台才能实现通知。如果不需要那就是观察者模式,如果需要那就是发布订阅模式。

它们的存在必然是有其理由的,就观察者模式而言,观察者与被观察者之间由于直接建立联系,那么势必会造成二者的耦合,而发布订阅模式确实完全的解耦,因为它依赖于第三方环境。

这并不是说哪一种模式好,或者说哪一种模式差。当我们需要在两个对象之间建立起稳定、适合的关系时我们就应该选择观察者模式,而当我们需要完全分离两个对象则最好选择发布订阅模式。

还是那句话,在适合的场景下选择适合的模式才是正确的,而不是为了使用而使用。

实验挑战

由于观察者模式理解起来并不困难,因此我们这里就不再让大家去写这方面的代码。

在这里希望大家能去阅读下上面提到的 vue 关于这一部分内容相关代码,其相关的源码路径是:

  1. src\core\observer\index.ts
  2. src\core\observer\watcher.ts

为了避免部分同学访问不了 github 的情况,我会将源码打包放进本实验最后的源码部分

实验总结

本实验通过家长群的例子为大家详细的介绍了观察者模式,并且还简单介绍了热门框架 VUE 对该模式相关应用的源码。旨在帮助大家建立深刻的认识。最后还详细的区分了观察者模式与发布订阅模式,让大家不会混淆这二者。希望同学们能够通过本实验的学习,真正掌握这两个设计模式。

本节实验源码压缩包下载链接:观察者模式源码

posted @   雨晨*  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
点击右上角即可分享
微信分享提示