这是我参与「第四届青训营」笔记创作活动的的第1天
一、什么是设计模式
1.1 设计模式概念
设计模式是在软件开发过程中为一些重复出现的软件设计问题提供的合理、有效的解决方案,让代码有更好的可复用性、可读性、可维护性,设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。
1.2 设计模式分类
《设计模式:可复用面向对象软件的基础》中提到23种设计模式并划分了三个类型:
- 创建型——如何创建一个对象
- 结构型——如何灵活的将对象组装成较大的结构
- 行为型——负责对象间的高效通信和职责划分
二、前端设计模式
2.1 浏览器当中的设计模式
- 单例模式
- 发布者订阅模式
2.1.1 什么是单例模式
定义:确保一个类仅有一个实例,并提供一个访问它的全局访问点,也就是一个全局访问对象
举例:
浏览器当中的window对象就是一个全局唯一的对象
应用场景:缓存、全局状态管理等
下面来介绍一下缓存的简单实现
import { api } from './utils'
export class Request {
static instance: Requset // 保存当前实例对象
private cache: Record<string, string> // 存储缓存值
constructor() {
this.cache = {} //初始化缓存内容
}
// 静态方法:用于创建 该类的实例对象
// 在实例化该类时调用该静态方法,目的就是避免创建多个实例化对象
static getInstance() {
// 是否存在实例化对象
if (this.instance) {
return this.instance;
}
this.instance = new Requset();
return this.instance;
}
// 缓存实现
public async request(url: string){
// 判断当前 url 上一次是否请求过,如果请求过,则直接返回上一次保存的结果
if (this.cache[url]) {
return this.cache[url]
}
// 没有请求过,则发送请求
const response = await api(url)
// 将请求结果保存到当前类的 cache 属性当中,便于重复请求时,过多的向服务器端发送请求,进而提高性能
this.cache[url] = response
return response
}
}
缓存的使用:
// 只调用一次 /user/1 接口,返回时间会比较长,因为没有缓存
test1(async () => {
const request = Requset.getInstance()
const startTime = Date.now()
await request.request("/user/1")
const endTime = Date.now()
const costTime = endTime - startTime
console.log(constTime)
})
// 现在调用两次 /user/1 接口,重复调用一次,第一次时间还是会比较长,但是第二次,由于已将该接口返回的结果进行了保存,则会直接返回结果,不会再向服务器发送二次请求
test2(async () => {
const request = Requset.getInstance()
await request.request('/user/1')
const startTime = Date.now()
const request1 = Requset.getInstance()
await request1.request("/user/1")
const endTime = Date.now()
const costTime = endTime - startTime
console.log(constTime)
})
2.1.2 什么是发布者订阅模式
定义:一种订阅机制(消息范式),可在被订阅对象发生变化时通知订阅者。 应用场景:从系统架构之间的解耦,到业务中一些实现模式,像邮件订阅,上线订阅等等,应用广泛
下面来简单的实现一下:
class ZEventBus {
constructor() {
// 保存需要监听的事件名称
this.eventBus = {}
}
on(eventName, eventCallback, thisArg) {
// 1. 在eventBus中通过事件名称取出对应的函数
const handles = this.eventBus[eventName]
// 当没有取出对应的函数时,就需要将对应的函数保存起来
if (!handles) {
handles = [] // 如果没有对应的函数,将其赋值为空数组
this.eventBus[eventName] = handles // 在evenBus中通过事件名称将其保存起来
}
// 将当前事件的回调以及传入的this保存
// 在此处进行push,而不是在if里面进行push,是因为如果同一个事件名被监听多次,
// if判断,当第二次事件监听时,不成立,就无法将第二个或更多次的函数无法保存,所以需要将其写在if外面
handles.push({
eventCallback,
thisArg
})
}
off(eventName, eventCallback) {
// 取出事件名对应的处理函数
let handles = this.eventBus[eventName]
if (!handles) return
// 遍历函数数组
handles.forEach((handler, index) => {
if (handler.eventCallback === eventCallback) {
handles.splice(index, 1)
}
})
}
emit(eventName, ...payload) {
// 通过事件名取出需要执行的函数数组
const handles = this.eventBus[eventName]
if (!handles) return
//
handles.forEach(handler => {
handler.eventCallback.apply(handler.thisArg, payload)
})
}
}
// test
const eventBus = new ZEventBus()
eventBus.emit('aaa', 123, 321)
function handler() {
console.log('监听aaa事件函数执行', this)
}
eventBus.on('aaa', handler)
2.2 Javascript中的设计模式
- 原型模式
- 代理模式
- 迭代器模式
2.2.1 原型模式
定义:复制已有对象来创建新的对象
应用场景:JavaScript对象创建的基本模式
原型链当中的原型对象,每个构造函数都会有属于自己的原型对象(prototype), 利用该构造函数所缔造的实例对象当中的(__proto__)会指向构造函数的原型对象(prototype),至此通过该构造函数缔造的所有对象都可以通过原型链来访问原型对象当中所有属性和方法。
2.2.2 代理模式
定义:可自定义控制原对象的访问方式,并且允许在更新前后作一些额外处理。
应用场景:监控、代理工具、前端框架实现等。
在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层, 通过这个中间层来进行对原对象的访问。
2.2.3 迭代器模式
定义:在不暴露数据类型的情况下访问集合中的数据
应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口
迭代就是循环,是集合或者数组最基本的功能,js已经对集合/数组数据结构封装好了迭代器。迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
2.3 前端框架中的设计模式
- 代理模式
- 组合模式
2.3.1 代理模式
定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。 应用场景:监听属性值的变化,例如状态管理工具库等
2.3.2 组合模式
定义:可多个对象组合使用,也可对单个对象独立使用 应用场景:DOM、前端组件、文件目录、部门
总结
- 设计模式中最核心的要素并非设计的结构,而是设计的思想。
- 灵活的使用设计模式会让软件将变得更加灵活,模块之间的耦合度将会降低,效率会提升,开销会减少。