设计模式在前端开发中的实践(十二)——单例模式

960 阅读5分钟

单例模式

单例模式是前端开发中常用的设计模式,单例模式本身也比较简单,因此对于很多同学来说,掌握度也是比较高的。

根据我7年的开发经验,我大致总结出单例模式2种高频使用场景:第一种场景是不需要用户创造它的实例对象,直接供用户调用,这种场景基本上都是用在封装工具类。第二种场景是需要用一些行为进行限制,比如如果一个界面上出现多个相同的UI组件(而逻辑上只出现一个才是合理的)就是明显的bug,但是为了降低调用者的心智负担,我们在设计的API内部实现的时候对其进行控制,然后使用我们代码的开发者就只需要无脑的调用就可以了,能够促进API设计的更加友好。

1、基本概念

单例模式,保证一个类仅有一个实例,并提供一个访问它的全局节点。

通常我们可以让一个全局变量使得一个对象被访问,但它不能被防止你实例化多个对象,一个最好的方式就是,让类自身负责保存它的唯一实例。

这个类可以保证没有其它实例可以被创建。

它的UML图如下:

singleton-pattern.png

从上图中可以看出,将其构造方法私有化,这样外界就无法实例化它了,并且暴露出了一个访问它唯一实例的方法

2、代码示例

class Singleton {
  /**
   * 内部持有全局唯一的实例
   */
  private instance: Singleton | null = null;
  /**
   * 私有化构造函数
   */
  private constructor() {}
  /**
   * 暴露访问其唯一实例的访问方法
   * @returns
   */
  getInstance(): Singleton {
    return this.instance || (this.instance = new Singleton());
  }
}

这个代码范式仅仅是使用TS根据上述的UML图实现的,而实际上,JavaScript比较灵活,因此前端在实现单例模式的时候,往往可以很简单,不比拘泥于上述的UML图。

3、前端开发中的实践

3.1 单例的Notice组件

一个大家比较熟悉的场景就是前端的Notice组件了,比如Element UIMessage组件,如果频繁的执行(用户点击的过快的话), 就会出现以下这种场景:

singleton-pattern-example.png

我个人觉得这种交互是比较糟糕的,但是Element UI的设计团队为了把最大的灵活度交给开发者,它并没有在实现的时候就保证其单例,因此,我们可以使用单例模式对Message组件进行封装。

因此,我们需要使用单例模式对Message组件进行封装:

import Vue from "vue";
/**
 * 单例的Message组件
 */
class SingletonMessage {
  static instance = null;

  constructor() {
    // 不允许当前类实例化
    throw new Error("this class can not called by new");
  }

  static show(options) {
    // 如果当前实例存在则什么事儿都可以不做了
    if (this.instance) {
      return;
    }
    let config;
    if (typeof options === "string") {
      config = {
        message: options,
        onClose: () => {
          // 做一些清理工作
          this.instance = null;
        },
      };
    } else {
      const { onClose, ...others } = options;
      config = {
        ...others,
        onClose: (...args) => {
          // 处理额外的清理工作
          this.instance = null;
          // 处理默认的参数
          typeof onClose === "function" && onClose.apply(this, args);
        },
      };
    }
    this.instance = Message(config);
  }

  static close() {
    if (!this.instance) {
      return;
    }
    this.instance.close();
  }
}

Vue.prototype.$singletonMessage = SingletonMessage;

看得仔细的同学可能会觉得上面的代码跟单例模式的UML的表示还是有一些差别的,切记学设计模式不要死板(很多时候,我们都在借鉴其设计思想),使用设计模式最大的动机在于将我们的代码写的易于维护,如果应用了设计模式反而使得我们的代码维护成本更高了,那就应该反思是不是做错了。

此例受制于Element UI的限制,Message组件每次关闭的时候都会移除DOM,所以看起来好像并不是那么“纯”,因此仅借鉴了单例模式的思想,达到了业务预期。

除此之外,还有些对象也是全局单例的,可能你每天都在用到,但你并没有在意,它就是->Math对象,LocalStorage对象,SessionStorage对象。

3.2 封装Bridge

我们团队的运营活动是运行在App中的webview中的,因此我们需要和原生的代码进行通信,于是客户端就像H5的环境中注入了一个bridge对象。

如果运营活动的开发者直接操作这个bridge对象的话,它需要去关注很多业务逻辑,比如bridge怎么初始化,是否初始化成功,在没有初始化成功的时候怎么降级处理,这些如果都让业务开发者自行处理的话,开发效率就太低了。

于是,我们就可以用一个统一的类来封装整体的逻辑,并且这个类不需要多次初始化。

// bridge对象的封装,因都是一些业务代码,我就不向大家展示了
class JsBridge {}

export class GlobalBridge {
  static instance: GlobalBridge

  static getInstance() {
    if (!this.instance) {
      this.instance = new JsBridge()
    }
    return this.instance
  }
}

除此之外,在很多团队统一封装axios也是可以采用单例模式,就可以参考这种方式。

总结

在文章的开头就向大家阐述了,单例模式主要的应用场景,单例模式是一种很简单的设计模式,也是每一个前端开发者必须要掌握的设计模式。

在实际的开发中,我们可以采用惰性单例设计。所谓惰性单例就是说,并不是系统一运行起来就立即去创建单例的类,而是当外部调用者需要的时候再创建,这将在某种程度上优化系统的初始化时间。

如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。