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

110 阅读8分钟

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

前言

本文浅述几种设计模式,均用了实例来增加可读性,重点是理解设计模式的各种思想。

设计模式

概念

软件设计中常见问题的解决方案模型

  • 历史经验总结
  • 和特定语言无关

分类

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

浏览器中的设计模式

一、单例模式

概述: 保证一个类仅有一个实例,并提供一个访问它的全局访问点;常常运用与缓存和全局状态管理

模式特点

  1. 类只有一个实例
  2. 全局可访问该实例
  3. 自行实例化(主动实例化)
  4. 可推迟初始化,即延迟执行(与静态类/对象的区别)

1.1、如何用单例模式来实现请求缓存?

 import { api } from "./utils"  //定义了一个api 500ms会返回一个值
 
 exprot class Requset { //定义了一个Requset类
     static instance: Request;  //静态方法存储全局唯一一个实例
     private cache:  Record<string , string>; //缓存对象,存储缓存值
     constructor(){
         this.cache = {} ; //初始化一个空的缓存内容
     }
     
     //真正实现单例模式
     static getInstance(){ //创建静态方法gitInstance,后面就不使用new Requset来创建对象,而是使用Requset.gitInstance来创建对象
         if(this.instance){
             return this.intance;
         } //如果instence存在就返回instence
         this.instance = new Requset();
         return this.instance;  //如果不存在就新建一个自身,并且返回
     }
     
     //真正缓存的实现
     public async request(url: string){ //url作为key
         if(this.cache[url]){  //如果缓存池中有url的内容
             return this.cache[url];  //返回缓存中的值
         }
         const response = await api(url);  //否则就调用api
         this.cache[url] = response;  //并写入缓存中
         
         return response;  //返回aip返回的值
     }
 }

实现方式: 使用一个变量存储类实例对象(值初始为 null/undefined )。进行类实例化时,判断类实例对象是否存在,存在则返回该实例,不存在则创建类实例后返回。多次调用类生成实例方法,返回同一个实例对象。

1.2、那么该类如何作为单例模式来使用的?

1、没有缓存时
    text("shuould" response more than 500ms with class",async () =>{  
    
    const request =Requset.getInstnce();  //创建对象
    
    const starTinme = Date.now(); //记录时间
    await request.request("/user/1");  //调用接口
    cost endTime = Date.now(); 
    
    const costTime = endTime - startTime;
    
    expect(costTime).toBeGreaterThanOrEqual(500);   //调用一次返回时间超过500ms 
    });

2、有缓存时
    text("shuould" response quicly second time with class",async () =>{
    //两次不同的调用
    const request1 =Requset.getInstnce();  
    await request1.request("/user/1");
    
    const starTinme = Date.now();
    const request2 =Requset.getInstnce();
    await request2.request("/user/1");
    const endTime = Date.new()
    
    const costTime = endTime - startTime;
    
    expect(costTime).toBeLessThan(50); //时间很小,认为已经拿到缓存值   
    })

二、单例模式的改进

2.1、改进后的请求缓存

     import { api } from "./utils"  //定义了一个api 500ms会返回一个值
     //定义字面量,空的缓存,作为全局唯一的对象
     const cache: Record<string , string> = {}; 
     
     //缓存的实现
     export const request async (url:string)  => {
         if(cache[url]){ //如果缓存池中有url的内容
             return cache[url]  //返回缓存中的值
         }
         
         cost response = await api(url);   //否则就调用api
         
         cache[url] = response; //并写入缓存中
         return response; //返回aip返回的值
     } 

而改进后的调用方发基本一致。

2.2、如何使用?

和单例模式基本一至就不过多赘述

由此可见,可以不使用class也能满足单例模式的条件,重点在于理解单例模式的核心思想

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

概述: 一种订阅机制 ,时通知订阅者 。可在被订阅对象发生变化;

应用场景: 从系统架构之间的解耦 , 到业务中一些实现模式 , 像邮件订阅 , 上线订阅等等 , 应用广泛 。

模式特点: 发布者----(桥梁)--->订阅者

3.1、如何实现发布订阅?

     //订阅者被通知的函数,只是一个类型订阅
    type Notify = (user:User) =>viod; 
    
    export class User {
        name : string;
        status : "offLine" | "onLine";
        //订阅他的人的和上线需要通知的函数
        fottowers : { user:User ;notify:Notify }[]; 
        
        //初始化一个新的user,只传入name,status,follwers
      constructor(name : string) {
        this.name = name;
        this. status = "offtine";
        this .followers = []; //订阅他的人的空数组
        
        //订阅他的方法
      subscribe(user : User , notify : Notify){
       //给对应的用户添加自己和对应订阅函数
        user.fottowers.push({ user,notify}); 
        }
      //表示上线的方法  
    ontine() {
        this.status = "onLine"; 
          //通知所有订阅自己的人调用一下订阅函数  
        this.fottowers .forEach(({ notify }) =>{
            notify (this);
            });
         }
      }

实现方式: 实际上,只要我们曾经在 DOM 节点上面绑定过事件函数,那我们就曾经使用过发布—订阅模式。例如我们在空白页面某处执行了点击操作,不管你点的地方有没有按钮,他都会触发许多事件。但我们都知道,点击空白处是不会发生任何响应的,这是因为我们没有给这个操作绑定相关的订阅者,或者说没有人关心你点击空白处是为了干什么。

3.2、如何使用发布订阅?

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();
    const mockNotifyUser2 = jest.fn();
    //user3上线,传入了一个假的通知User1的函数,并且user1和user3可以互相订阅
    user1. subscribe(user3, mockNotifyUser1) ;
    user2. subscribe(user3, mockNotifyUser2) ;
    // user3上线
    user3. onLine();
    //通知订阅user3 的用户(user1 和user2)
    expect (mockNotifyUser1) . toBeCaLledWith(user3) ;
    expect (mockNotifyUser2) . toBeCalledWith(user3) ;

JavaScript的设计模式

一、原型模式

定义: 复制已有对象来创建新的对象,主要应用于JS中创建的基本样式

1.1、如何实现用原型模式创建线上订阅中的客户?

    //定义字面量对象baseUser
  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) ;
      });
    },
  };
  //独立的方法creatUser,只接收name作为参数
  export const createUser = (name: string) = {
  //创建user,基于一个已有对象创建一个新的对象,相对于继承关系
    const user: User = Object.create (baseUser);
    
    user.name = name ;//先命名一个新的名称
    user.followers = [ ] ; //再将followers初始化
    
    return user ;
  };

实现方式: js中基于原型链的继承的原理本质上就是对继承过来的类的属性和方法的共享,并不是对属性和方法的拷贝。

1.2、如何使用?

和发布订阅的模式基本一至就不过多赘述

在表示上线的方法中,出现一个方法完成两件事情的情况,不太好维护,那么我们可以进行一个优化,即代理模式

二、代理模式

定义: 可自定义控制对象访问方式,并且允许在更新前后做一些额外的处理,主要在监控,代理工具,前端框架的实现等方面应用

2.1、如何使用代理模式实现用户状态订阅

    export const createProxyUser = (name: string) = {
    //返回带着用户的user
      const user = new User(name) ;
      
    const proxyUser = new Proxy(user ,{
    set: (target, prop: keyof User, vaLue) => {
        //target就是被代理的对象,以及对应的属性名称属性值
        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 }) 7 {
        notify(user);
    });
  }
};

    return proxyUser;
};

实现方式: 因为代理要实现和被代理对象实际处理一样的效果,所以,在实现代理对象时,原对象有的方法,代理对象一样有,这样可以保证,用户在操作代理对象时就像在操作原对象一样。

有关Proxy的用法

2.2、如何使用?

和发布订阅的模式基本一至就不过多赘述

三、迭代器模式

定义 在不暴露数据类型的情况下访问集合中的数据,多用于多数据类型,列表,数,等提供操作接口

模式特点

  1. 为遍历不同数据结构的 “集合” 提供统一的接口;
  2. 能遍历访问 “集合” 数据中的项,不关心项的数据结构

3.1如何实现用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 {
    //每次for of 迭代的时候会返回值value done(迭代是否完成)
      next: () => {
        //每次拿到子组件上的node都会push到list的组件上(遍历)
        while ((node = list . shift())) {
          node.children.Length > 0 && List.push( ...node.children) ;
          return { vaLue: node,  done: false };
        }
        return { value: null, done: true };
      },
    };
  }
}

实现方式: 首先调用遍历对象 [Symobo.iterator]() 方法,拿到遍历器对象; 每次循环,调用遍历器对象 next() 方法,得到 {value: ..., done: ... } 对象

3.2、怎么使用

可使用 for 循环遍历就行

前端框架中的设计模式

一、vue组件实现计数器

<tempLate>
  <button @aclick= "count++ ">count is:{{ count }}</ button>
</template> 
<script setup Lang="ts"> 
import { ref } from "vue"

const count = ref(0);
</script>

二、组合模式

定义: 可多个对象组合使用,也可单个对象独立使用,多应用于DOM前端组件文件目录,部门等

三、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>
    );

整个app都可以用DOM来渲染

总结

感受设计模式中的思想,更好的应用和提升自己,总结出抽象模式相对简单,但将抽象模式应用到场景中却非常困难;真正优秀的开源项目需要创作者不断学习设计并不断实践

小试牛刀

使用组件实现一个文件夹结构

要求:

  • 每个文件夹可以包含文件和文件夹
  • 文件有大小
  • 可获取每个文件夹下文件的整体大小

知识补充