# 什么是设计模式
设计模式是软件设计中常见问题的解决方案模型,是历史经验的总结,与特定语言无关
设计模式大致分为 23 种设计模式
- 创建型 —— 如何高效灵活的创建一个对象
- 结构型 —— 如何灵活的将对象组装成较大的结构
- 行为型 —— 负责对象间的高效通信和职责划分
# 浏览器中的设计模式
# 单例模式
单例模式 —— 存在一个全局访问对象,在任意地方访问和修改都会反映在这个对象上
最常用的其实就是浏览器中的 window 对象,提供了对浏览器的操作的封装,常用于缓存与全局状态管理等
# 单例模式实现请求缓存
用单例模式实现请求缓存:相同的 url 请求,希望第二次发送请求的时候可以复用之前的一些值
首先创建 Request 类,该类包含一个创建单例对象的静态方法 getinstance,然后真正请求的操作为 request 方法,向 url 发送请求,若缓存中存在该 url 则直接返回,反之则缓存到该单例对象中。可以看到如下是上节课讲过的语法~
import {api} from './utils'; | |
export class Request { | |
static instance: Request; | |
private cache: Record<string, string>; | |
constructor() { | |
this.cache = {}; | |
} | |
static getinstance() { | |
if(this.instance) { | |
return this.instance; | |
} | |
this.instance = new Request(); // 之前还未有过请求,初始化该单例 | |
return this.instance; | |
} | |
public async request(url:string) { | |
if(this.cache[url]) { | |
return this.cache[url]; | |
} | |
const response = await api(url); | |
this.cache[url] = response; | |
return response; | |
} | |
} |
实际中使用如下:利用 getInstance 静态方法创建该单例对象,并测试起执行时间进行对比。
ps: 这里的测试是使用 Jest 进行的,其中用到了部分 expect 的 api,可以通过文档了解其用途
// 不预先进行请求,测试其时间。 | |
test('should response more than 500ms with class', async() => { | |
const request = Request.getinstance(); // 获取 / 创建一个单例对象(若之前未创建过则创建) | |
const startTime = Date.now(); | |
await request.request('/user/1'); | |
const endTime = Date.now(); | |
const costTime = endTime-startTime; | |
expect(costTime).toBeGreaterThanOrEqual(500); | |
}); | |
// 先进行一次请求,在测试第二次请求的时间 | |
test('should response quickly second time with class', async() => { | |
const request1 = Request.getinstance(); | |
await request1.request('/user/1'); | |
const startTime = Date.now(); // 测试这一部分的时间 | |
const request2 = Request.getinstance(); | |
await request2.request('/user/1'); | |
const endTime = Date.now(); // | |
const costTime = endTime-startTime; | |
expect(costTime).toBeLessThan(50); | |
}); |
而在 js 中,我们也可以不用 class 写,这是因为传统的语言中无法 export 出来一个独立的方法等,只能 export 出来一个类
// 不用 class?可以更简洁 | |
import {api} from './utils'; | |
const cache: Record<string,string> = {}; | |
export const request = async (url:string) => { | |
if(cache[url]) { // 与 class 中一致 | |
return cache[url]; | |
} | |
const response = await api(url); | |
cache[url] = response; | |
return response; | |
}; | |
// 使用,可以看出来该方法也符合单例模式,但更加简洁。 | |
test('should response quickly second time', async() => { | |
await request('/user/1'); | |
const startTime = Date.now(); // 测试这一部分的时间 | |
await request('/user/1'); | |
const endTime = Date.now(); | |
const costTime = endTime-startTime; | |
expect(costTime).toBeLessThan(50); | |
}); |
# 发布订阅模式(观察者模式)
应用非常广泛的一种模式,在被订阅对象发生变化时通知订阅者,常见场景很多,从系统架构之间的解耦到业务中的一些实现模式、邮件订阅等等。类似于添加事件
# 发布订阅模式实现用户上线订阅
举个实际应用的例子:通过该模式,我们可以实现用户的相互订阅,在该用户上线时调用相应的通知函数。
如图创建了一个 User 类, 构造器中初始状态置为离线,其拥有一个 followers 对象数组,包括了该用户订阅的所有 {用户,调用函数},每次在该用户上线时,遍历其 followers 进行通知
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 = []; | |
} | |
subscribe(user:User, notify: Notify) { | |
user.followers.push({user, notify}); | |
} | |
online() { // 该用户上线 调用其订阅函数 | |
this.status = "online"; | |
this.followers.forEach( ({notify}) => { | |
notify(this); | |
}); | |
} | |
} |
测试函数:还是用 jest,创建假的订阅函数进行测试(
test("should notify followers when user is online for multiple users", () => { | |
const user1 = new User("user1"); | |
const user2 = new User("user2"); | |
const user3 = new User("user3"); | |
const mockNotifyUser1 = jest.fn(); // 通知 user1 的函数 | |
const mockNotifyUser2 = jest.fn(); // 通知 user2 的函数 | |
user1.subscribe(user3, mockNotifyUser1); // 1 订阅了 3 | |
user2.subscribe(user3, mockNotifyUser2); // 2 订阅了 3 | |
user3.online(); // 3 上线,调用 mockNotifyUser1 和 mockNotifyUser2 | |
expect(mockNotifyUser1).toBeCalledWith(user3); | |
expect(mockNotifyUser2).toBeCalledWith(user3); | |
}); |
# JavaScript 中的设计模式
# 原型模式
可以想到 javascript 中的常见语言特性:原型链,原型模式指的其实就是复制一个已有的对象来创建新的对象,这在对象十分庞大的时候会有比较好的性能(相比起直接创建)。常用于 js 中对象的创建
# 原型模式创建上线订阅中的用户
首先,创建一个原型,可以看到这个原型相比起之前的来说没有定义构造器。
// 原型模式,当然要有原型啦 | |
const baseUser:User = { | |
name: "", | |
status: "offline", | |
followers: [], | |
subscribe(user, notify) { | |
user.followers.push({user, notify}); | |
}, | |
online() { // 该用户上线 调用其订阅函数 | |
this.status = "online"; | |
this.followers.forEach( ({notify}) => { | |
notify(this); | |
}); | |
} | |
} |
导出在该原型之上创建对象的函数,该函数接受一个 name 参数,利用 Object.create () 使用原型来创建一个新对象,并在其基础上进行增加或修改
// 然后导出在该原型之上创建对象的函数 | |
export const createUser = (name:string) => { | |
const user:User = Object.create(baseUser); | |
user.name = name; | |
user.followers = []; | |
return user; | |
}; |
实际使用:可以看到将 new User 变成了 createUser
test("should notify followers when user is online for user prototypes", () => { | |
const user1 = createUser("user1"); | |
const user2 = createUser("user2"); | |
const user3 = createUser("user3"); | |
const mockNotifyUser1 = jest.fn(); // 通知 user1 的函数 | |
const mockNotifyUser2 = jest.fn(); // 通知 user2 的函数 | |
user1.subscribe(user3, mockNotifyUser1); // 1 订阅了 3 | |
user2.subscribe(user3, mockNotifyUser2); // 2 订阅了 3 | |
user3.online(); // 3 上线,调用 mockNotifyUser1 和 mockNotifyUser2 | |
expect(mockNotifyUser1).toBeCalledWith(user3); | |
expect(mockNotifyUser2).toBeCalledWith(user3); | |
}); |
# 代理模式
可自定义控制队员对象的访问方式,并且允许在更新前后做一些额外处理,常用于监控、代理工具、前端框架等等。JS 中有自带的代理对象:Proxy() ,在红宝书中代理那章也有详细阐述。
# 代理模式实现用户状态订阅
还是上述观察者模式的例子,可以使用代理模式对其进行优化,让他的 online 函数只做一件事:更改状态为上线。
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 = []; | |
} | |
subscribe(user:User, notify: Notify) { | |
user.followers.push({user, notify}); | |
} | |
online() { // 该用户上线 调用其订阅函数 | |
this.status = "online"; | |
// this.followers.forEach( ({notify}) => { | |
// notify(this); | |
// }); | |
} | |
} |
创建 User 的一个代理:ProxyUser
Proxy 函数说明
target
要使用
Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理
p
的行为。
// 创建代理,监听其上线状态的变化 | |
export const createProxyUser = (name:string) => { | |
const user = new User(name); // 正常的 user | |
// 代理的对象 | |
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; | |
} |
# 迭代器模式
在不暴露数据类型的情况下访问集合中的数据,常用于数据结构中拥有多种数据类型(列表、
树等),提供通用的操作接口。
# 用 for of 迭代所有组件
用到了 Symbol.iterator 该迭代器可以被 for...of
循环使用。
定义一个 list 队列,每次从队首取个节点出来,如果这个节点有孩子结点将其全部添加到队尾,每次调用 next 都返回一个结点~详见代码
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 }; | |
}, | |
}; | |
} | |
} |
使用场景如下:通过 for of 迭代 body 中的所有子元素
test("can iterate root element", () => { | |
const body = new MyDomElement("body"); | |
const header = new MyDomElement("header"); | |
const main = new MyDomElement("main"); | |
const banner = new MyDomElement("banner"); | |
const content = new MyDomElement("content"); | |
const footer = new MyDomElement("footer"); | |
body.addChildren(header); | |
body.addChildren(main); | |
body.addChildren(footer); | |
main.addChildren(banner); | |
main.addChildren(content); | |
const expectTags: string[] = []; | |
for(const element of body) { // 迭代 body 的所有元素,需要包含 main 中的子元素 | |
if(element) { | |
expectTags.push(element.tag); | |
} | |
} | |
expect(expectTags.length).toBe(5); | |
}); |
# 前端框架中的设计模式(React、Vue...)
# 代理模式
与之前讲的 Proxy 不太一样
# Vue 组件实现计数器
<template> | |
<button @click="count++">count is:</button> | |
</template> | |
<script setup lang="ts"> | |
import {ref} from "vue"; | |
const count = ref(0); | |
</script> |
上述代码,为什么 count 能随点击而变化?这就要说到前端框架中对 DOM 操作的代理了:
更改 DOM 属性 -> 视图更新
更改 DOM 属性 -> 更新虚拟 DOM -Diff-> 视图更新
如下就是前端框架对 DOM 的一个代理,通过其提供的钩子可以在更新前后进行操作:
<template> | |
<button @click="count++">count is:</button> | |
</template> | |
<script setup lang="ts"> | |
import { ref, onBeforeUpdate, onUpdated } from "vue"; | |
const count = ref(0); | |
const dom = ref<HTMLButtonElement>(); | |
onBeforeUpdate(() => { | |
console.log("Dom before update", dom.value?.innerText); | |
}); | |
onUpdated(() => { | |
console.log("Dom after update", dom.value?.innerText); | |
}); | |
</script> |
# 组合模式
可以多个对象组合使用,也可以单个对象独立使用,常应用于前端组件,最经典的就是 React 的组件结构:
# React 组件结构
还是计数器的例子~
export const Count = () => { | |
const [count, setCount] = useState(0); | |
return ( | |
<button onClick={() => setCount((count) => count+1)}> | |
count is: {count} | |
</button> | |
); | |
}; |
该 Count,既可以独立渲染,也可以渲染在 App 中,后者就是一种组合
function App() {
return (
<div className = "App">
<Header />
<Count />
<Footer />
</div>
);
}
# 总结感想
下面是老师的一些总结:
设计模式不是银弹,总结出抽象的模式听起来比较简单,但是想要将抽象的模式套用到实际的场景中却非常困难,现代编程语言的多编程范式带来了更多的可能性,我们要在真正优秀的开源项目中学习设计模式并不断实践
这节课讲了浏览器和 js 中常用的设计模式,包括单例模式、观察者模式、原型模式、代理模式、迭代器模式等,还讲了设计模式究竟有什么用~在我看来,从实际的项目中学习设计模式确实是一种比较好的方法。
本文引用的大部分内容来自吴立宁老师的课