前端设计模式 | 青训营

42 阅读6分钟

1.设计模式概述

1.什么是设计模式

设计模式是对软件设计开发过程中反复出现的某类问题的通用解决方案。设计模式更多的是指导思想和方法论,而不是现成的代码,当然每种设计模式都有每种语言中的具体实现方式。
软件中常见问题的解决方案模型:

  • 历史经验的总结
  • 与特定语言无关
2.设计模式背景
  1. 模式语言:城镇、建筑、建造
  2. 设计模式:可复用面向对象软件的基础

📘推荐书籍:Design Patterns:Elements of Reusable Object-Oriented Software,1994

3.设计模式分类

23种设计模式

  • 创建型:如何创建一个对象,根据实际情况使用合适的方式创建对象
  • 结构型:如何让灵活的将对象组装成较大的结构,通过识别系统中组件间的关系来简化系统的设计
  • 行为型:负责对象间的高效通信和职责划分,用于识别对象之间常见的交互模式并加以实现,增加了这些交互的灵活性

4.浏览器中的设计模式

1.单例模式

定义:全局唯一访问对象,当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场
❗ 注意:单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。
应用场景:缓存,全局状态管理等
案例:用单例模式实现请求缓存

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

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

定义:一种订阅机制,可在被订阅对象发生变化时,通知订阅者(比如你订阅了某个博主的频道,你就会接收到她内容更新的推送)
image.png
应用场景:从系统架构之间的解耦,松耦合的设计让对象之间的依赖关系变得更加灵活,到业务中一些实现模式,像邮件订阅,上线订阅等,应用广泛
比如给 DOM 元素绑定事件的 addEventListener( ) 方法:

target.addEventListener(type,listener[,options]);

target 就是被观察对象 subject,listener 就是观察者 observer
案例:简单的发布模式实现

// 定义一个发布者对象
var publisher = {
  // 定义一个事件列表
  events: {},

  // 添加事件到列表中
  addEvent: function(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  },

  // 从事件列表中删除事件
  removeEvent: function(event, callback) {
    if (this.events[event]) {
      for (var i = 0; i < this.events[event].length; i++) {
        if (this.events[event][i] === callback) {
          this.events[event].splice(i, 1);
          break;
        }
      }
    }
  },

  // 发布事件
  publishEvent: function(event, data) {
    if (this.events[event]) {
      for (var i = 0; i < this.events[event].length; i++) {
        this.events[event][i](data);
      }
    }
  }
};

// 定义一个订阅者对象
var subscriber = {
  // 处理事件的回调函数
  handleEvent: function(data) {
    console.log(data);
  }
};

// 订阅一个事件
publisher.addEvent('event1', subscriber.handleEvent);

// 发布一个事件
publisher.publishEvent('event1', 'Hello, world!');

// 取消订阅一个事件
publisher.removeEvent('event1', subscriber.handleEvent);

2.JavaScript中的设计模式

1.原型模式(JS中常见,但是其他语言就不常用)

定义:使用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象(复制已有对象来创建新的对象),JavaScript中的原型模式使用 Object.create() 方法来创建一个对象,并且可以通过修改原型链上的属性和方法来修改新对象的行为。
优势:可以减少对象创建的时间和成本,通过克隆来创建新对象,提高了性能和效率
应用场景: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.follwers = [];

    return user;
  }

2.代理模式**(Proxy Pattern)**

定义:它允许在不改变原始对象的情况下,通过引入一个代理对象来控制对原始对象的访问。**可自定义控制对原对象的访问方式,并允许在更新前后做一些额外处理,**ES6中也增加了 Proxy 的功能。
应用场景:监控、代理工具、前端框架实现(Vue中对数据操作并劫持)等
要实现代理模式需要三部分:

  1. Real Subject:真实对象
  2. Proxy:代理对象
  3. Subject接口:Real Subject 和 Proxy都需要实现的接口,这样Proxy才能被当成Real Subject的“替身”使用

案例:使用代理模式实现用户状态订阅

3.迭代器模式

定义:在不暴露数据类型的情况下访问集合中的数据
image.png
应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口
ES6提供了迭代循环语法 for...of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。
案例:使用 for of 迭代所有组件

3.前端框架中的设计模式

1.代理模式

案例:Vue组件实现计数器

<template>
  <button @click="count++" ref="dom">count is : {{count}}</button>
</template>

前端框架中对 DOM 操作的代理
image.png
虚拟DOM = 代理DOM (无需自己操作)
代理的体现:DOM 更新前后的钩子对比

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

<template>
  <button @click="count++" ref="dom">count is : {{count}}</button>
</template>

2.组合模式

定义:可多个对象组合使用(组合成树形结构),也可单个对象独立使用
应用场景:DOM、前端文件、文件目录、部门
实现组合模式通常有两种方式:

  1. 使用类继承:通过定义一个抽象的 Component 类和两个具体的 Composite 和 Leaf 类来实现。Composite 类继承自 Component 类,并且拥有一个子节点列表。Leaf 类继承自 Component 类,并且没有子节点。这种方式的实现比较传统,但是需要使用类继承,可能会导致类层次结构比较复杂。
  2. 使用对象组合:通过使用对象字面量和原型继承等技术来实现。这种方式可以不需要类继承,而是使用对象字面量和原型链来模拟组合模式的结构,比较灵活,但是代码可能比较冗长。

案例:React 的组件结构

4.小结

  • 总结出抽象的模式相对比较简单,但是想要将抽象的模式套用到场景中却非常困难
  • 现代编程语言的多范式编程带来了更多可能性
  • 真正优秀的开源项目学习设计模式并不断实践

🔗拓展:前端设计模式实战
随时代发展,出现了很多变化,传统的代理模式就不太能出现在工作当中,主要是要去学习思想,学习总结其中的精华,其实我个人认为设计模式这块其实蛮复杂的,其实这次梳理完也没有很明白,可能就是经验不足也是一方面,还是得继续学习!😲