设计模式创建型之单例模式
1.单例模式介绍
由于单例模式比较简单,我们可以首先给出它的定义:一个类仅有一个实例,并提供一个访问它的全局访问点。
一个类仅有一个实例?那么我们在什么情况下需要使用它呢,在前端又有什么应用场景?
我们可以先举一些例子:
1、由于 Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,因此文件的处理必须通过唯一的实例来进行。
2、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
当你想控制实例的数量时,可以考虑单例模式。
现在请考虑这样一个情况,你有一个全局使用的类频繁地创建与销毁。如果大量使用该类则难免造成系统资源的浪费。可以通过创建一个全局可使用的实例来避免此问题出现。
大致了解什么是单例模式后,让我们看看在前端应该怎么实现吧。
如果要全局使用一个实例,那就需要该类具备判断全局范围是否已经创建过实例的能力。像下面这样就是不行的:
class SinglePeople {
constructor(name) {
this.name = name;
}
eat = () => {
console.log(`${this.name}可以吃东西!`);
}
}
const person1 = new SinglePeople("张三");
const person2 = new SinglePeople("张三");
console.log(person1 === person2); // false
可以看到,虽然我们本意是想创建张三这个人,但是通过 SinglePeople 创建的实例却是不一样的,由此表明这样的创建方式是无法实现单例模式的。
因此我们给类增加一个属性 instance ,用于判断是否已经存在实例,就像这样:
class SinglePeople {
constructor(name) {
this.name = name;
}
static getInstance(name) {
// 判断是否已经创建过实例
if (!SinglePeople.instance) {
// 实例不存在则创建
SinglePeople.instance = new SinglePeople(name)
}
// 实例存在则直接返回
return SinglePeople.instance;
}
eat = () => {
console.log(`${this.name}可以吃东西!`);
}
}
现在让我们来试验一下:
const person1 = SinglePeople.getInstance("张三");
const person2 = SinglePeople.getInstance("张三");
console.log(person1 === person2); // true
可以看到,通过这样方式是可以实例单例模式的。
关于单例模式在前端中典型应用的代表,那么就不得不提到 Redux 和 Vuex 中的 State。我们这里仅用 Redux 来举例。
2.单例模式的应用之 Redux
单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
上面这段直接引自于官网关于对单一数据源的介绍。
在 react 中,父子组件间通信一般会使用 props ,或者通过 context 隔层传递数据。兄弟组件也可以通过状态提升的方式来共享数据。
但当组件非常多、组件间关系复杂、且嵌套层级很深的时候,这种原始的通信方式会使我们的逻辑变得复杂难以维护,此时就可以考虑使用 Redux 来保存数据。而 Store 就是用于存放数据的唯一数据源。
关于 Redux 详细的内容,大家可以自行选择参考 Redux 官网或者翻阅相关源码,这里提到 Redux 仅为了提供一个大家平常接触较多的例子。如果对 Redux 不了解也不影响,只需要牢记:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。这是单例模式的典型应用场景。
3.实现一个 Storage 存储
本小节将会实现一个基于 localStorage 全局的的存储类。
class MyStorage {
// 核心部分,实例不存在则创建,已创建则直接返回
static getInstance = () => {
if (!MyStorage.instance) {
MyStorage.instance = new MyStorage();
}
return MyStorage.instance;
}
setState(key, value) {
window.localStorage.setItem(key, value)
}
getState(key) {
return window.localStorage.getItem(key);
}
// 测试函数
show = () => {
console.log(window.localStorage);
}
}
可以看到,上面代码的核心部分不变,仍然是 getInstance 这个静态函数。
下面可以测试一下:
// 创建一个实例
const store1 = MyStorage.getInstance();
store1.show(); // Storage {length: 0}
store1.setState("name", "张三");
store1.setState("age", 24);
store1.setState("sex", "男");
store1.show(); // Storage {age: '24', name: '张三', sex: '男', length: 3}
console.log(store1.getState(("name"))); // 张三
// 再创建一个实例,判断是否会重复创建
const store2 = MyStorage.getInstance();
console.log(store1 === store2); // true
store2.show(); // Storage {age: '24', name: '张三', sex: '男', length: 3}
// 为 store2 中放入新值,观察 store1 的数据
store2.setState("work", "程序员")
store1.show(); // Storage {work: '程序员', name: '张三', age: '24', sex: '男', length: 4}
store2.show(); // Storage {work: '程序员', name: '张三', age: '24', sex: '男', length: 4}
可以看到,上面真正实现了 store1 和 store2 的数据共享。
现在来小结一下:在单例模式中,只需要牢记两点:
1、一个类仅有一个实例,并提供一个访问它的全局访问点。
2、并且理解这段代码:
static getInstance = () => {
if (!MyStorage.instance) {
MyStorage.instance = new MyStorage();
}
return MyStorage.instance;
}