「青训营」前端设计模式

187 阅读5分钟

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

设计模式一个有 23 种,分为三个类型:

  1. 创建型:如何创建一个对象
  2. 结构型:如何灵活地将对象组装为较大的结构
  3. 行为型:负责对象间的高效通信和职责划分

浏览器中的设计模式

单例模式

定义:全局唯一的访问对象

应用场景:缓存,全局状态管理等

实现请求缓存

在页面中,我们会有多处地方发送 url 请求,我们希望第二次发送请求时复用之前的缓存。

  1. 用 Class 实现:
// 定义 api,500ms 后返回
import { api } from "./utils";

export class Requset {
  // 定义请求实例
  static instance: Requset;
  // 定义缓存
  private cache: Record<string, string>;

  // 初始化缓存内容
  constructor() {
    this.cache = {}
  }

  // 创建对象的方法,使用 Request.getInstance() 创建对象,不使用 new Request()
  static getInstance() {
    // 有实例则返回实例
    if (this.instance) {
      return this.instance;
    }
    // 没有则创建实例
    this.instance = new Requset();
    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 = Requset.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 = Requset.getInstance();
  await request1.request("/user/1");

  const startTime = Date.now();
  const request2 = Requset.getInstance();
  await request2.request("/user/1");
  const endTime = Date.now();

  const costTime = endTime - startTime;

  expect(costTime).toBeLessThan(50);
});
  1. 不使用 Class:
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);
});

相较于需要使用类的传统语言,JavaScript 中可以更灵活地实现设计模式。

发布订阅模式

定义:一种订阅机制,可在被订阅对象发生变化时通知订阅者

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

实现用户上线订阅

type Notify = (user: User) => void;

export class User {
  name: string;
  status: "offline" | "online";
  // user 订阅自己的人,notify 上线时的通知函数
  followers: { user: User; notify: Notify }[];

  constructor(name: string) {
    this.name = name;
    this.status = "offline";
    this.followers = [];
  }

  // 订阅参数中的 user
  subscribe(user: User, notify: Notify) {
    user.followers.push({ user, notify });
  }

  // 上线
  online() {
    // 状态改为 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");

  // 通知 user1 和 user2 的函数
  const mockNotifyUser1 = jest.fn();
  const mockNotifyUser2 = jest.fn();

  // user1 订阅了 user3 的上线,传入通知 user1 的函数
  user1.subscribe(user3, mockNotifyUser1);
  user2.subscribe(user3, mockNotifyUser2);

  // user3 上线
  user3.online();

  // user3 会调用通知 user1 的函数
  expect(mockNotifyUser1).toBeCalledWith(user3);
  expect(mockNotifyUser2).toBeCalledWith(user3);
});

JavaScript 中的设计模式

是指 JavaScript 中提供的 API 能帮我们实现的模式。

原型模式

定义:复制已有的对象来创建新的对象

应用场景: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) => {
  // Object.create(obj) 会根据已有的对象返回一个新的对象
  // baseUser 是原型,与新创建的对象是继承关系
  const user: User = Object.create(baseUser);

  user.name = name;
  user.followers = [];

  return user;
};
test("should notify followers when user is online for multiple users", () => {
  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();

  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";
  //   this.followers.forEach(({ notify }) => {
  //     notify(this);
  //   });
  // }

  // online 只做上线一件事情,单一职责原则
  online() {
    this.status = 'online'
  }
}

// 实现通知
export const createProxyUser = (name: string) => {
  const user = new User(name);

  // 使用 new Proxy() 实现代理
  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 迭代所有组件

// 浏览器中的 DOM 结构
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 迭代时调用的函数
      next: () => {
        while ((node = list.shift())) {
          // 层序遍历
          node.children.length > 0 && list.push(...node.children);
          // value 是迭代出的值,done 是指迭代是否完成
          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,React 等框架中的设计模式

代理模式

Vue 组件实现计数器

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

<script setup lang="ts">
import { ref } from "vue ";
const count = ref(0);
</script>

为什么 count 改变后页面会自动渲染?

没有框架之前,我们通过监听 click 事件,通过 innerText 手动改变 DOM 的属性:

image.png

前端框架对 DOM 操作进行代理,模板中的 DOM 都是 Proxy 代理后的虚拟 DOM,我们的操作的 DOM 是虚拟 DOM,之后通过 Diff 对视图更新:

image.png

DOM 更新前后的钩子也是代理模式的体现。

组合模式

定义:可多个对象组合使用成为一个单独的对象,也可以单个对象独立使用

应用场景: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>
  );
}

Count 组件可以独立渲染,可以在 App 这个更大的结构中渲染。

总结

设计模式不是银弹。

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