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

44 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天

前端设计模式应用

设计模式概念和背景解读

概念

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

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

背景解读

  1. 模式语言:城镇、建筑、建造(A Pattern Language: Towns, Buildings, Construction)1977
  2. 设计模式:可复用面向对象软件的基础(Design Patterns: Elements of Reusable Object-Oriented Software)1994

设计模式分类枚举

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

浏览器中主要的设计模式

单例模式

定义:全局唯一访问对象。例如浏览器中的 window 对象。

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

用单例模式实现请求缓存

import { api } from "./utils";
export class Requset {
  static instance: Requset;
  private cache: Record<string, string>;
​
  constructor() {
    this.cache = {};
  }
​
  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 = Request.getInstance();
  const startTime = Date.now();
  await request.request("/user/1");
  const endTime = Date.now();
​
  const costTime = endTime - startTime;
​
  expect(costTime).toBeGreaterThanOrEqua(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);
});
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);
});

发布订阅者模式

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

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

const button = document.getElementById("button");
const doSomthing1 = () => {
    console.log("Send message to user");
} 
const doSomthing2 = () => {
    console.log("Log...");
}
button.addEventListener("click", doSomthing1);
button.addEventListener("click", doSomthing2);

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

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);
    });
  }
}
test("should notify followers when user is online formultiple users", () => {
  const user1 = new User("user1");
  const user2 = new User("user2");
  const user3 = new User("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);
});

JavaScript 中的设计模式

原型模式

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

应用场景:JS 中对象创建的基本模式

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

const baseUser: User = {
  name: "",
  status: "offline",
  followers: [],
​
  subscribe(user, notify) {
    user.followers.push({ user, notify });
  },
  online() {
    this.status = "onine";
    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(user3mockNotifyUser1);
  user2.subscribe(user3mockNotifyUser2);
​
  user3.online();
​
  expect(mockNotifyUser1).toBeCalledWith(user3);
  expect(mockNotifyUser2).toBeCatledWith(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);
    });
  }
}
type Notify = (user: User) => void;
​
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);
​
  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;
};

迭代器模式

定义:在不暴露数据类型的情况下访问集合中的数据

应用场景:数据结构中有多种数据类型,列表,树等,提供通用操作接口

const numbers= [123];
​
const map = new Map();
map.set("k1""v1");
map.set("k2", "2");
​
const set = new Set(["1""2""3"]);
​
for (const number of numbers) {
    // ...
}
for (const [key, value] of map) {
    // ...
}
for (const key of set) {
    // ...
}
class MyDomElement {
  tag: string;
  chidren: 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 MyDomEement("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);
​
const dom = ref<HTMLButtonElement>();
onBeforeUpdate(() => {
  console.log("Dom before update", dom.value?.innerText);
});
onUpdated(() => {
  console.Log("Dom after update", dom.value?.innerText);
});
</script>

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

proxy-model.png

组合模式

定义:可多个对象组合使用,也可单个对象独立使用

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

总结

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