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

69 阅读5分钟

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

什么是设计模式

设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发过程中面临的一般问题的结局方案。这些解决方案是众多软件开发人员经过相当长的一段时间的经验和错误总结出来的。

设计模式大致分为23种设计模式

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

浏览器中的设计模式

单例模式

定义:全局唯一访问对象
应用场景:缓存,全局状态管理等。

用单例模式实现请求缓存

相同的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出来一个类

import {api} from './utils';
const cache: Record<string,string> = {};
export const request = async (url:string) => {
    if(cache[url]) { 
        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、当发布者状态改变,会自动通知所有订阅者。2、发布者和订阅者耦合性降低。
缺点:1、创建订阅者需要消耗一定的事件和内存
2、虽然可以弱化对象间的关系,但是过度使用,会使得代码不好维护。
原理 1、确定发布者
2、给发布者添加缓存列表,用于存放回调函数来通知订阅者
3、发布消息,发布者遍历这个缓存列表,依次促发里面存放的订阅者回调函数。

var EventCenter = (function(){
    var events = {};
    /*
    {
      my_event: [{handler: function(data){xxx}}, {handler: function(data){yyy}}]
    }
    */
    // 绑定事件 添加回调
    function on(evt, handler){
        events[evt] = events[evt] || [];
        events[evt].push({
            handler:handler
        })
    }
    function fire(evt, arg){
        if(!events[evt]){
            return 
        }
        for(var i=0; i < events[evt].length; i++){
            events[evt][i].handler(arg);
        }
    }
    function off(evt){
        delete events[evt];
    }
    return {
        on:on,
        fire:fire,
        off:off
    }
}());

var number = 1;
EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'次');
});
EventCenter.off('click');   //  只绑定一次
EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'次');
});

EventCenter.fire('click', '绑定');

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);
        });
    }
}
export const createUser = (name:string) => {
    const user:User = Object.create(baseUser);
    user.name = name;
    user.followers = [];
    return user;
};

测试

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(); 
    const mockNotifyUser2 = jest.fn(); 
    user1.subscribe(user3, mockNotifyUser1); 
    user2.subscribe(user3, mockNotifyUser2);
    user3.online();  // 3上线,调用mockNotifyUser1和mockNotifyUser2
    expect(mockNotifyUser1).toBeCalledWith(user3);
    expect(mockNotifyUser2).toBeCalledWith(user3);
});

代理模式

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

代理模式实现用户状态订阅

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";
    }
}
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 迭代所有组件

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 };
            },
        };
    }
}
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) {
        if(element) {
            expectTags.push(element.tag);
        }
    }
    expect(expectTags.length).toBe(5);
});

前端框架中的设计模式

代理模式

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>
    );
};
function App() {
    return (
        <div className = "App">
        	<Header />
            <Count />
            <Footer />
        </div>
    );
}