什么是设计模式浏览器中的设计模式
单例模式
单例模式实现请求缓存 发布订阅模式(观察者模式)
发布订阅模式实现用户上线订阅 Javascript中的设计模式
原型模式
原型模式创建上线订阅中的用户 代理模式
代理模式实现用户状态订阅 迭代器模式
用for of 迭代所有组件 前端框架中的设计模式(React、Vue...)
代理模式
Vue组件实现计数器 组合模式
React组件结构 总结感想
将自己在掘金上发的笔记搬了过来:前端设计模式应用
什么是设计模式设计模式是软件设计中常见问题的解决方案模型,是历史经验的总结,与特定语言无关
设计模式大致分为23种设计模式
创建型——如何高效灵活的创建一个对象结构型——如何灵活的将对象组装成较大的结构行为型——负责对象间的高效通信和职责划分 浏览器中的设计模式 单例模式
单例模式——存在一个全局访问对象,在任意地方访问和修改都会反映在这个对象上
最常用的其实就是浏览器中的window对象,提供了对浏览器的操作的封装,常用于缓存与全局状态管理等
单例模式实现请求缓存用单例模式实现请求缓存:相同的url请求,希望第二次发送请求的时候可以复用之前的一些值
首先创建Request类,该类包含一个创建单例对象的静态方法getinstance,然后真正请求的操作为request方法,向url发送请求,若缓存中存在该url则直接返回,反之则缓存到该单例对象中。可以看到如下是上节课讲过的语法~
import {api} from './utils';export class Request { static instance: Request; private cache: Record
实际中使用如下:利用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
应用非常广泛的一种模式,在被订阅对象发生变化时通知订阅者,常见场景很多,从系统架构之间的解耦到业务中的一些实现模式、邮件订阅等等。类似于添加事件
发布订阅模式实现用户上线订阅举个实际应用的例子:通过该模式,我们可以实现用户的相互订阅,在该用户上线时调用相应的通知函数。
如图创建了一个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组件实现计数器
上述代码,为什么count能随点击而变化?这就要说到前端框架中对DOM操作的代理了:
更改DOM属性 -> 视图更新
更改DOM属性 -> 更新虚拟DOM -Diff-> 视图更新
如下就是前端框架对DOM的一个代理,通过其提供的钩子可以在更新前后进行操作:
组合模式
可以多个对象组合使用,也可以单个对象独立使用,常应用于前端组件,最经典的就是React的组件结构:
React组件结构还是计数器的例子~
export const Count = () => { const [count, setCount] = useState(0); return ( );};
该Count,既可以独立渲染,也可以渲染在App中,后者就是一种组合
function App() { return (
下面是老师的一些总结:
设计模式不是银弹,总结出抽象的模式听起来比较简单,但是想要将抽象的模式套用到实际的场景中却非常困难,现代编程语言的多编程范式带来了更多的可能性,我们要在真正优秀的开源项目中学习设计模式并不断实践
这节课讲了浏览器和js中常用的设计模式,包括单例模式、观察者模式、原型模式、代理模式、迭代器模式等,还讲了设计模式究竟有什么用~在我看来,从实际的项目中学习设计模式确实是一种比较好的方法。
本文引用的大部分内容来自吴立宁老师的课