单例模式
单例模式是前端开发中常用的设计模式,单例模式本身也比较简单,因此对于很多同学来说,掌握度也是比较高的。
根据我7年的开发经验,我大致总结出单例模式2种高频使用场景:第一种场景是不需要用户创造它的实例对象,直接供用户调用,这种场景基本上都是用在封装工具类。第二种场景是需要用一些行为进行限制,比如如果一个界面上出现多个相同的UI组件(而逻辑上只出现一个才是合理的)就是明显的bug,但是为了降低调用者的心智负担,我们在设计的API内部实现的时候对其进行控制,然后使用我们代码的开发者就只需要无脑的调用就可以了,能够促进API设计的更加友好。
1、基本概念
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局节点。
通常我们可以让一个全局变量使得一个对象被访问,但它不能被防止你实例化多个对象,一个最好的方式就是,让类自身负责保存它的唯一实例。
这个类可以保证没有其它实例可以被创建。
它的UML
图如下:
从上图中可以看出,将其构造方法私有化 ,这样外界就无法实例化它了,并且暴露出了一个访问它唯一实例的方法。
2、代码示例
ts
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 UI
的Message
组件,如果频繁的执行(用户点击的过快的话), 就会出现以下这种场景:
我个人觉得这种交互是比较糟糕的,但是Element UI
的设计团队为了把最大的灵活度交给开发者,它并没有在实现的时候就保证其单例,因此,我们可以使用单例模式对Message
组件进行封装。
因此,我们需要使用单例模式对Message
组件进行封装:
js
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怎么初始化,是否初始化成功,在没有初始化成功的时候怎么降级处理,这些如果都让业务开发者自行处理的话,开发效率就太低了。
于是,我们就可以用一个统一的类来封装整体的逻辑,并且这个类不需要多次初始化。
ts
// bridge对象的封装,因都是一些业务代码,我就不向大家展示了
class JsBridge {}
export class GlobalBridge {
static instance: GlobalBridge
static getInstance() {
if (!this.instance) {
this.instance = new JsBridge()
}
return this.instance
}
}
除此之外,在很多团队统一封装axios也是可以采用单例模式,就可以参考这种方式。
总结
在文章的开头就向大家阐述了,单例模式主要的应用场景,单例模式是一种很简单的设计模式,也是每一个前端开发者必须要掌握的设计模式。
在实际的开发中,我们可以采用惰性单例设计。所谓惰性单例就是说,并不是系统一运行起来就立即去创建单例的类,而是当外部调用者需要的时候再创建,这将在某种程度上优化系统的初始化时间。
如果大家喜欢我的文章,可以多多点赞收藏加关注,你们的认可是我最好的更新动力,😁。