前端设计模式应用|青训营笔记

64 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第5天

本文是针对青训营《前端设计模式应用》这节课所学的课后总结,通过吴立宇老师深入浅出的讲解,让我了解了许多前端涉及的设计模式。

简介

设计模式是软件设计中常见问题的解决方案模型,是历史经验的总结,与特定语言无关。

设计模式的23种模式分为三种:

  • 创建型——如何高效灵活的创建一个对象
  • 结构型——如何灵活的将对象组装成较大的结构
  • 行为型——负责对象间的高效通信和职责划分

浏览器中的设计模式

单例模式

单例模式:存在一个全局访问对象,多应用于缓存与状态管理等场景;最常用的就是浏览器中的window对象。

image.png 老师在课上举了一个单例模式实现请求缓存的例子:

  1. 用传统语言先创建Request类,包含一个创建单例对象的静态方法getinstance;
  2. 然后真正请求的操作为request方法,并进行判断是否缓存。
  3. 并使用Jest进行测试。(其中用到了部分expect的api,可以通过文档了解其用途)
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;
    }
}

// 不预先进行请求,测试其时间。
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中,则可以直接export出来一个独立的方法:

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);
});

发布订阅者模式

一种应用非常广泛的模式,在被订阅者发生变化后通知订阅者,类似于添加事件,从系统架构之间的解耦到业务中的一些实现模式、邮件订阅等都有涉及。

  1. 创建一个User类,初始状态为离线;订阅该用户的用户被push进订阅者对象followers;
  2. 每次在该用户上线时,遍历其followers进行通知。
  3. 同样用jest进行测试。
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);
        });
    }
}

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中的设计模式

原型模式

复制已有对象来创建新的对象,常用于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);
        });
    }
}
export const createUser = (name:string) => {
    const user:User = Object.create(baseUser);
    user.name = name;
    user.followers = [];
    return user;
};

代理模式

可自定义控制对原对象的访问方式,并且允许在更新前后做一些额外处理,常用于监控、代理工具、前端框架等等。

简单的来说,就是将复杂的函数中的某些功能用代理模式进行代理,以达到简化。

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);
        // });
    }
}

在这里创建一个代理:ProxyUser,其中target属性包装目标对象,handler包装各种函数行为。这样,就是原本online函数中的内容简化了。

// 创建代理,监听其上线状态的变化
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;
}

迭代器模式

在不暴露数据类型的情况下访问集合中的数据,常用于遍历各种数据类型,如下述代码:

const numbers = [1,2,3];

const map = new Map();
map.set("k1","v1")
map.set("k2","v2")

const set = new Set(["1","2","3"]);

for (const number of numbers){
    //.....
}
for (const [key,value] of map){
    //.....
}
for (const key of set){
    //.....
}

前端框架中的设计模式

代理模式

这里用一个Vue组件实现计数器的案例展示:

<template>
	<button @click="count++">count is:{{ count }}</button>
</template>
<script setup lang="ts">
import {ref} from "vue";
const count = ref(0);
</script>

这里Vue框架通过对DOM操作进行了代理:

更改DOM属性 -> 视图更新

更改DOM属性 -> 更新虚拟DOM -Diff-> 视图更新

通过钩子函数也能看出count的前后变化:

<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>

182fcb1f-5f70-4535-8797-5e7f8f0c66ef.gif

组合模式

  • 多个对象组合成为单个对象,也可单个对象单独使用
  • DOM结构,前端组件,文件目录

总结

通过这节课,让我了解了什么是设计模式及设计模式的用处,吴老师通过案例讲解了各个设计模式实现的过程,正如老师课上讲的,设计模式总结起来比较简单,但真正用到实际场景却非常困难,这让我了解了想要深入理解设计模式就要不断的实践。