设计模式行为型之观察者模式
实验介绍
本实验为大家介绍的是观察者模式,我们通过家长群的例子为大家仔细地梳理了这一模式的基本概念,帮助大家建立基本认识。虽然为大家介绍这一模式在前端热门框架 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()
}
上面仅仅是小部分源码,大体的对 Observer
和 watcher
这两部分进行了核心概述,在设计模式中限于篇幅,不可能对源码进行详细介绍,因此对 VUE 源码感兴趣的同学可以自行从 GitHub 下载。
当然了,如果你现在还处在前端的初级阶段,或者是对 VUE 还不太了解,那么可以暂时先跳过对 VUE 源码的分析,这一部分并不会影响对观察者模式的理解。之所以介绍这一块内容,是为了让大家看一看观察者模式在前端中的真实应用场景。
监听器 是主动观察?而一开始的家长群是被动观察?
观察者模式与发布订阅模式
通常情况下,你可以简单的认为这两种模式是一个概念,因为在核心思想上这二者确实没有本质的差别。
但是它们之间也确实存在细微的区别。我们现在来简单区分下。
还是以上面的家长群为例,我们把这种通过老师建群,把家长都拉进一个群里从而实现的通知模式称为观察者模式。那么什么情况下是发布订阅模式呢?
实际上,如果老师没有建群,而是在学校的官方网站上发布了相关信息,而家长们需要通过访问这个网站才能获取到老师发布分消息的情况,这就是发布订阅模式。
可能大家看到这里大家会有一点迷糊,我们用精炼的语言来抽取这两种情况:
- 观察者模式:不涉及第三方平台,建立被观察者与观察者的之间联系,例如老师在群里直接发消息,家长们直接在群里接收消息。
- 发布订阅模式:通过统一的第三方平台,发布者在平台上发布消息,订阅者在平台上查看消息,这种情况下发布者与订阅者之间没有直接的联系。例如上面的学校官网就是一个第三方平台。
总结来说,它们二者之间的区别就在于是否是通过第三方平台才能实现通知。如果不需要那就是观察者模式,如果需要那就是发布订阅模式。
它们的存在必然是有其理由的,就观察者模式而言,观察者与被观察者之间由于直接建立联系,那么势必会造成二者的耦合,而发布订阅模式确实完全的解耦,因为它依赖于第三方环境。
这并不是说哪一种模式好,或者说哪一种模式差。当我们需要在两个对象之间建立起稳定、适合的关系时我们就应该选择观察者模式,而当我们需要完全分离两个对象则最好选择发布订阅模式。
还是那句话,在适合的场景下选择适合的模式才是正确的,而不是为了使用而使用。
实验挑战
由于观察者模式理解起来并不困难,因此我们这里就不再让大家去写这方面的代码。
在这里希望大家能去阅读下上面提到的 vue 关于这一部分内容相关代码,其相关的源码路径是:
src\core\observer\index.ts
src\core\observer\watcher.ts
为了避免部分同学访问不了 github 的情况,我会将源码打包放进本实验最后的源码部分
实验总结
本实验通过家长群的例子为大家详细的介绍了观察者模式,并且还简单介绍了热门框架 VUE 对该模式相关应用的源码。旨在帮助大家建立深刻的认识。最后还详细的区分了观察者模式与发布订阅模式,让大家不会混淆这二者。希望同学们能够通过本实验的学习,真正掌握这两个设计模式。
本节实验源码压缩包下载链接:观察者模式源码
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性