前端设计模式 | 青训营

83 阅读4分钟

设计模式

设计模式是软件设计中常⻅问题的典型解决⽅案。 它们就像能根据需求进⾏调整的预制蓝图, 可⽤于解

决代码中反复出现的设计问题

设计模式与⽅法或库的使⽤⽅式不同, 你很难直接在⾃⼰的程序中套⽤某个设计模式。 模式并不是⼀段

特定的代码, ⽽是解决特定问题的⼀般性概念。 我们可以根据模式来实现符合⾃⼰程序实际所需的解决⽅

  • 创造型:如何更好地创建一个对象
  • 结构型:如何灵活地将对象组装成较大的结构,并同时保持结构的灵活和⾼效
  • 行为型:负责对象间的高效通信和职责划分

浏览器中的设计模式

单例模式

全局唯一访问对象,主要应用于缓存、全局状态管理等

缓存实践

const cache: Record<string, string> = {};

export const request = async (url: string) => {
  if (cache[url]) {
    return cache[url]
  }
  const response = await fetch(url);
  cache[url] = response.toString()
  return cache[url]
}

在调用的时候我们可以使用 await request(url) 来调用这个函数,在 cache[url] 存在的情况下会直接调用 cache[url] 而不是重新执行 fetch(url) 来获取数据

发布订阅模式

是一种订阅机制,可以在被订阅者的对象内容发生改变的时候通知订阅者改变的内容,主要应用于从系统架构之间的解耦,到业务中一些实现模式,比如邮件订阅,上线订阅等等

const button = document.getElementById("button")

const method1 = () => {
  console.log("Sending...");
}

button?.addEventListener("click", method1)

在这个示例中,我们可以把 button 当成是一个被订阅的对象,而订阅者就是那一个个函数,当被订阅者 button 发生被点击事件的时候,就会执行相对应的函数,输出相应的结果

用户上线订阅实践

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 = [];
  }

  subscibe(user: User, notify: Notify) {
    user.followers.push({ user, notify })
  }

  online() {
    this.status = "online";

    this.followers.forEach(({ notify }) => {
      notify(this);
    });
  }
}

在上述代码中我们可以看到订阅是通过调用 subscribe 这个函数实现的,在调用时需要传入两个参数,一个是被订阅者的信息,一个是订阅函数,当被订阅者上线的时候会调用 online 这个函数,然后在被订阅者的 followers 中遍历订阅函数并且逐个调用,告知订阅者上线的信息

JS 中的设计模式

原型模式

复制已有对象来创建新的对象,主要应用于 JS 中对象的创建

基于原型模式创建上线订阅

通过 Object.create() 基于 baseUser 作为新对象的原型,在创建新对象的过程中都通过调用 createUser()

const baseUser: User = {
  name: "",
  status: "offline",
  followers: [],
  
  subscibe(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
}

代理模式

可以子当以控制对象的访问方式,并且允许在更新前后做一些额外处理,主要应用于监控(比如前端发出的所有请求的成功率)、代理工具和前端框架的实现等等

基于代理模式实现状态订阅

在之前的版本中我们可以看到 online() 这个函数中除了实现将 用户的状态修改为 online 之外还进行了其他操作,这一写法并不便于后续的代码优化

online() {
  this.status = "online";
  this.followers.forEach(({ notify }) => {
    notify(this);
  });
}

将提醒 followers 的函数抽离出来,通过代理的方式进行

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
}

可以看到主要的过程是通过 Proxy() 来绑定需要被代理的对象 user,然后通过 set 来控制当设置 user.status 的属性值的时候需要被调用的函数,实现了功能的抽离,方便后续增加新的功能迭代

  • 注:在 Proxy 中主要有两个方法 set 和 get,set 方法会在对象进行赋值时候被调用,get 方法会在对象进行取值时候被调用

迭代器模式

在不暴露数据类型的情况下访问集合中的数据,主要应用于数据结构中有多种数据类型,列表,树等情况下,提供通用的操作接口

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

我们先定义一个 MyDomElement 类,[Symbol.iterator]() 是 js 中用于将组件变为可迭代的内置方法,next 是每次迭代的时候自动被调用的函数

const body = new MyDomElement("body")
const header = new MyDomElement("header")
const main = new MyDomElement("main")
const content = new MyDomElement("content")

body.addChildren(header)
body.addChildren(main)
main.addChildren(content)

const expectTags: string[] = [];
for(const element of body) {
  if (element) {
    expectTags.push(element.tag);
  }
}

然后我们就可以通过 for of 来实现对于组件树的迭代