了解设计模式 | 青训营笔记

50 阅读5分钟

设计模式概述

设计模式是根据软件设计中常见问题从历史性经验中总结出来的解决方案模型

设计模式是一种理论、方法、思想,与实现语言无关。

设计模式分类

目前总结的主流设计模式主要有23种,这些设计模式又可以分为3个类别:

  • 创建型,主要是为了灵活、高效的创建一个对象
  • 结构型,主要是为了灵活地将多个对象组装成更大的结构
  • 行为型,主要负责对象间的高效通信和职责划分

aefc6eb7f5ba13216d5f21051327816e.png

浏览器中的设计模式

浏览器中常用的设计模式有两种:单例模式、发布订阅模式

单例模式

单例模式就是指存在一个全局唯一访问对象,常用的应用场景:缓存、全局状态管理等。

用单例模式实现请求缓存实例:

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

使用js的方式简单实现单例模式:

import { api } from "./utils";

// cache作为全局唯一变量缓存
const cache: Record<string, string>;

export const request = async (url:string) => {
    if (cache[url]) {
        return cahce[url];
    }
    
    const response = await api(url);
    
    cache[url] = response;    
    return response;
}

发布订阅模式(观察者模式)

发布订阅模式是一种订阅机制,用于在订阅对象发生变化时,通知订阅者,常见的应用场景:如邮件订阅、上线订阅等。

用发布订阅模式实现用户上线订阅:

// 唤醒函数
type Notify = (user: User) => void;

export class User {
    name: string;
    status: "offline" | "online";
    // followers存储订阅对象及唤醒函数
    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);    // 唤醒当前订阅者
            // expect(...).toBeCalledWith(...) 测试一个函数是否被特定的参数调用
        });
    }
}

JavaScript中的设计模式

JavaScript中的设计模式指JavaScript中提供了api方便实现这些设计模式

原型模式

原型模式是指复制已有对象来创建新的对象,常见的应用场景有:JS中对象创建的基本模式。

用原型模式创建上线订阅中的用户:

const baseUser: User = {
    name: "";
    status: "offline";
    followers: [];

    subscribe(user, notify) {
        user.followers.push({ user, notify });
    },
    ontine() {
        this.status = "onLine";
        this.folLowers.forEach(({ notify }) => {
            notify(this);
        });
    },
};
export const createUser = (name: string) => {
    // js中Object类提供了crate方法来从已有的对象创建新的对象,两者为继承关系
    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方法,只更改状态,不再做额外操作
    online() {
        this.status = "online";
    }
}

// 创建用户方法不再使用new User()
export const creatProxyUser = (name: string) => {
    // 创建一个普通的user
    const user = new User(name);
    
    // 使用JS提供的new Proxy方法创建代理对象
    // 传入的参数:要代理的对象和代理操作(set方法(设置原对象属性时触发)和get方法(获取原对象属性时触发))
    const proxyUser = new Proxy(user, {
        // target: 设置对象,prop:属性名称,value:属性值
        set: (target, prop: keyof User, value) => {
            target[prop] = value;
            // 设置属性为status时,调用handler方法
            if (prop === "status") {
                notifyStatusHandlers(target, value);
            }
            return true;
        },
    });
    
    const notifyStatusHandlers = (user: User, status: "online" | "offline") => {
        // 根据状态触发事件,后期可以根据需要添加handler,可拓展性强
        if (status === "online") {
            user.followers.forEach(({ notify }) => {
                notify(this);
            });
        }
    };
    
    return proxyUser;
}

迭代器模式

迭代器模式是指在不暴露数据类型的情况下访问集合中的数据。常见的应用场景有:为多种数据类型提供通用的接口。

用迭代器模式实现for of迭代自定义组件:

class MyDomElement {
    tag: string;
    children: MyDomElement[];
    
    constructor(tag: string) {
        this.tag = tag;
        this.children = [];
    }
    
    addChildren(component: MyDomElement) {
        this.children.push(component);
    }
    
    // JS提供了内置方法[Symbol.iterator],用于让类变为可迭代的
    [Symbol.iterator]() {
        const list = [...this.children];
        let node;
        
        // 函数返回一个字典,包含一个key为next的函数
        return {
            next: () => {
                // 当list中仍有数据时,返回的字典中done为false
                while ((node = list.shift())) {
                    // 将结点的子节点也添加至迭代list
                    // 表达式1 && 表达式2:表达式1为true时执行表达式2
                    node.children.length > 0 && list.push(...node.children);
                    
                    return {value: node, done: false};
                }
                // 当list中没有数据后,返回的字典中done为true
                return {value: node, done: true};
            }
        }
    }
}

前端框架中的设计模式

代理模式(与JS中提供api实现的代理模式不同)

Vue组件实现计数器:

<template>
    // click监听事件为count变量自增
    <button @click="count++"> count is: {{ count }} </button>
</template>

<script setup lang="ts">
    import { ref } from "vue";
    // ref函数类似上述的createProxyUser,创建代理对象
    const count = ref(0);
</script>

前端框架中对DOM操作的代理

没有框架之前更新视图的过程: image.png 框架更新视图的过程: image.png 针对DOM的操作实际上是对代理后的DOM做更新

DOM更新前后的钩子:可以使用vue提供的onBeforeUpdateonUpdate高阶函数查看更新前后的状态。

组合模式

组合模式是指可以多个对象组合为单个对象使用,也可以单个对象独立使用,常见的应用场景有:DOM、前端组件、文件目录和部门目录等。

React的组件结构

count也使用了代理DOM,但是与Vue不同,未使用JS提供的Proxy api。 Count可以作为独立组件被渲染,也可以在更大的结构中被渲染。

export const Count = () => {
    const [count, setCount] = useState(0);
    
    return (
        <button onClick={() => setCount((count) => count + 1)}>
            count is: {count}
        </button>
    );
};

总结

设计模式不是银弹:

  • 总结出抽象的模式相对简单,但是想要将抽象的设计模式应用到场景中比较困难
  • 现代编程语言的多范式编程更加灵活
  • 学习设计模式需要多多阅读开源项目,不断实践