什麼是設計模式#
設計模式是軟體設計中常見問題的解決方案模型,是歷史經驗的總結,與特定語言無關
設計模式大致分為 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 靜態方法創建該單例對象,並測試其執行時間進行對比。
// 不預先進行請求,測試其時間。
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:{{ count }}</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:{{ count }}</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 中常用的設計模式,包括單例模式、觀察者模式、原型模式、代理模式、迭代器模式等,還講了設計模式究竟有什麼用~在我看來,從實際的項目中學習設計模式確實是一種比較好的方法。
本文引用的大部分內容來自吳立寧老師的課