设计模式
设计模式是软件设计中常⻅问题的典型解决⽅案。 它们就像能根据需求进⾏调整的预制蓝图, 可⽤于解
决代码中反复出现的设计问题
设计模式与⽅法或库的使⽤⽅式不同, 你很难直接在⾃⼰的程序中套⽤某个设计模式。 模式并不是⼀段
特定的代码, ⽽是解决特定问题的⼀般性概念。 我们可以根据模式来实现符合⾃⼰程序实际所需的解决⽅
案
- 创造型:如何更好地创建一个对象
- 结构型:如何灵活地将对象组装成较大的结构,并同时保持结构的灵活和⾼效
- 行为型:负责对象间的高效通信和职责划分
浏览器中的设计模式
单例模式
全局唯一访问对象,主要应用于缓存、全局状态管理等
缓存实践
const cache: Record<string, string> = {};
export const request = async (url: string) => {
if (cache[url]) {
return cache[url]
}
const response = await fetch(url);
cache[url] = response.toString()
return cache[url]
}
在调用的时候我们可以使用 await request(url) 来调用这个函数,在 cache[url] 存在的情况下会直接调用 cache[url] 而不是重新执行 fetch(url) 来获取数据
发布订阅模式
是一种订阅机制,可以在被订阅者的对象内容发生改变的时候通知订阅者改变的内容,主要应用于从系统架构之间的解耦,到业务中一些实现模式,比如邮件订阅,上线订阅等等
const button = document.getElementById("button")
const method1 = () => {
console.log("Sending...");
}
button?.addEventListener("click", method1)
在这个示例中,我们可以把 button 当成是一个被订阅的对象,而订阅者就是那一个个函数,当被订阅者 button 发生被点击事件的时候,就会执行相对应的函数,输出相应的结果
用户上线订阅实践
type Notify = (user: User) => void
export class User {
name: string;
status: "offline" | "online";
followers: { user: User; notify: Notify }[];
constructor(name: string) {
this.name = name;
this.status = 'offline';
this.followers = [];
}
subscibe(user: User, notify: Notify) {
user.followers.push({ user, notify })
}
online() {
this.status = "online";
this.followers.forEach(({ notify }) => {
notify(this);
});
}
}
在上述代码中我们可以看到订阅是通过调用 subscribe 这个函数实现的,在调用时需要传入两个参数,一个是被订阅者的信息,一个是订阅函数,当被订阅者上线的时候会调用 online 这个函数,然后在被订阅者的 followers 中遍历订阅函数并且逐个调用,告知订阅者上线的信息
JS 中的设计模式
原型模式
复制已有对象来创建新的对象,主要应用于 JS 中对象的创建
基于原型模式创建上线订阅
通过 Object.create() 基于 baseUser 作为新对象的原型,在创建新对象的过程中都通过调用 createUser()
const baseUser: User = {
name: "",
status: "offline",
followers: [],
subscibe(user, notify) {
user.followers.push({user, notify})
},
online() {
this.status = "online";
this.followers.forEach(({notify}) => {
notify(this)
})
},
}
export const createUser = (name: string) => {
const user: User = Object.create(baseUser);
user.name = name;
user.followers = [];
return user
}
代理模式
可以子当以控制对象的访问方式,并且允许在更新前后做一些额外处理,主要应用于监控(比如前端发出的所有请求的成功率)、代理工具和前端框架的实现等等
基于代理模式实现状态订阅
在之前的版本中我们可以看到 online() 这个函数中除了实现将 用户的状态修改为 online 之外还进行了其他操作,这一写法并不便于后续的代码优化
online() {
this.status = "online";
this.followers.forEach(({ notify }) => {
notify(this);
});
}
将提醒 followers 的函数抽离出来,通过代理的方式进行
export const createProxyUser = (name: string) => {
const user = new User(name);
const proxyUser = new Proxy(user, {
set: (target, prop: keyof User, value) => {
target[prop] = value;
if (prop === 'status') {
notifyStatusHandlers(target,value);
}
return true;
}
})
const notifyStatusHandlers = (user: User, status: "online" | "offline") => {
if (status === "online") {
user.followers.forEach(({notify}) => {
notify(user)
})
}
}
return proxyUser
}
可以看到主要的过程是通过 Proxy() 来绑定需要被代理的对象 user,然后通过 set 来控制当设置 user.status 的属性值的时候需要被调用的函数,实现了功能的抽离,方便后续增加新的功能迭代
- 注:在 Proxy 中主要有两个方法 set 和 get,set 方法会在对象进行赋值时候被调用,get 方法会在对象进行取值时候被调用
迭代器模式
在不暴露数据类型的情况下访问集合中的数据,主要应用于数据结构中有多种数据类型,列表,树等情况下,提供通用的操作接口
class MyDomElement {
tag: string;
children: MyDomElement[];
constructor(tag: string) {
this.tag = tag;
this.children = [];
}
addChildren(component: MyDomElement) {
this.children.push(component)
}
[Symbol.iterator]() {
const list = [...this.children];
let node;
return {
next: () => {
while ((node = list.shift())) {
node.children.length > 0 && list.push(...node.children);
return { value: node, done: false }
}
return { value: null, done: true }
}
}
}
}
我们先定义一个 MyDomElement 类,[Symbol.iterator]() 是 js 中用于将组件变为可迭代的内置方法,next 是每次迭代的时候自动被调用的函数
const body = new MyDomElement("body")
const header = new MyDomElement("header")
const main = new MyDomElement("main")
const content = new MyDomElement("content")
body.addChildren(header)
body.addChildren(main)
main.addChildren(content)
const expectTags: string[] = [];
for(const element of body) {
if (element) {
expectTags.push(element.tag);
}
}
然后我们就可以通过 for of 来实现对于组件树的迭代