努力让学习成为一种习惯,自信来源于充分的准备
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
前言
单例模式是设计模式中较为简单的一种。虽然简单,但在前端领域中,使用的场景有很多。我们一起来看看单例模式在前端开发中有哪些实践
单例模式
在实战篇之前,我们先说下什么是单例模式,单例模式有两个特点:
-
一个类有且只有一个实例
-
提供一个对外暴露的API用于访问该实例(严格定义来说为:提供一个访问它的全局节点,这意味着严格意义来讲类的构造函数是私有化的,不能通过它来实例化)
参考这里给出的uml图
我们可以基于以上特点写出下面的伪代码
class Singleton {
// 在类中添加一个私有静态成员变量用于保存单例实例
private static instance: Singleton
// 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用
private constructor() {}
// 声明一个公有静态构建方法用于获取单例实例
public static getInstance(): Singleton {
return this.instance || (this.instance = new Singleton())
}
}
但由于js本身是单线程,自身特性很灵活。因此js实现单例模式可以很简单,没必要拘泥上述规范。明白其设计原理,解决了什么问题才是关键
我们来看下具体实现代码
es5
// es5
function Singleton() {}
Singleton.getInstance = function () {
if (!Singleton.instance) {
Singleton.instance = new Singleton()
}
return Singleton.instance
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2);
es6
class Singleton {
static getInstance() {
if(!Singleton.instance) {
Singleton.instance = new Singleton()
}
return Singleton.instance
}
}
const s1 = Singleton.getInstance()
const s2 = Singleton.getInstance()
console.log(s1 === s2); // true
// 也可以基于 constructor(虽然不标准化,但本质都是一样的)
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this
}
return Singleton.instance
}
}
const s1 = new Singleton()
const s2 = new Singleton()
console.log(s1 === s2); // true
在上面实现的代码中,我们使用了静态属性、方法的方式存储、获取唯一实例,当然我们也可以使用闭包
Singleton.getInstance = (function () {
let instance
return function () {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()
实践场景
localStorage
对于localStorage,我们并不陌生,业务开发中会需要它本地存储数据。localStorage其实就是一个全局单例对象。我们可以模仿实现一下
es5
function StorageBase() {}
StorageBase.prototype.getItem = function(key) {
return this[key]
}
StorageBase.prototype.setItem = function (key, value) {
this[key] = value
}
var Storage = (function () {
var instance = null
return function () {
return instance || (instance = new StorageBase())
}
})()
// 这里用不用new无所谓
var s1 = new Storage()
var s2 = new Storage()
console.log(s1 === s2); // true
s1.setItem('a', 1)
console.log(s1.getItem('a')); // 1
console.log(s2.getItem('a')); // 1
es6
class Storage {
static instance = null
static getInstance = function () {
return this.instance || (this.instance = new Storage())
}
getItem(key) {
return this[key]
}
setItem(key, value) {
this[key] = value
}
}
const s1 = Storage.getInstance()
const s2 = Storage.getInstance()
console.log(s1 === s2); // true
s1.setItem('a', 1)
console.log(s1.getItem('a')); // 1
console.log(s2.getItem('a')); // 1
message弹框
我们先来看一道经典考察单例模式的面试题:实现一个全局唯一的Modal弹框
直接上代码:
// html
<div>
<button class="add">add</button>
<button class="close">close</button>
</div>
// js
class Model {
static instance
static getInstance () {
if (!this.instance) {
const model = document.createElement('div')
model.id = 'model'
model.style.display = 'none'
model.innerHTML = 'here! i am here!'
document.body.appendChild(model)
this.instance = new Model(model)
}
return this.instance
}
constructor(model) {
this.model = model
}
open() {
this.model.style.display = 'block'
}
close() {
this.model.style.display = 'none'
}
}
const addBtn = document.querySelector('.add')
addBtn.addEventListener('click', () => {
const model = Model.getInstance()
model.open()
})
const closeBtn = document.querySelector('.close')
closeBtn.addEventListener('click', () => {
const model = Model.getInstance()
model.close()
})
但大多数情况,我们业务中使用的弹窗、消息提示都是使用第三方封装好的组件,比如elementui的 dialog、message
有一个常见的场景是当我们elementui的message消息提示,调用接口报错的时候,如果用户手速过快连续点了许多次。就会出现这种情况
但其实这种交互体验并不好,理论上在上一次message实例销毁前,不应再次产生新的实例,基于这种情况。我们就可以利用单例模式进行改造,如果之前生成的message实例还存在,则不需要生成新的实例
import Vue from 'vue'
import { Message } from 'element-ui'
import type { ElMessageOptions, ElMessageComponent } from 'element-ui/types/message'
class SingletonMessage {
static instance: ElMessageComponent | null = null
private constructor() {
throw new Error('class SingletonMessage can not be called by new')
}
static show(options: string | ElMessageOptions) {
if (this.instance) return
let config: ElMessageOptions
if (typeof options === 'string') {
config = {
message: options,
onClose: () => {
this.instance = null
}
}
} else {
const { onClose, ...otherOptions } = options
config = {
...otherOptions,
onClose: (...args) => {
typeof onClose === 'function' && onClose.apply(this, args)
this.instance = null
}
}
}
this.instance = Message(config)
}
static close() {
this.instance && this.instance.close()
}
}
Vue.prototype.$singletonMessage = SingletonMessage
我们在axios的拦截器使用
const SingletonMessage = Vue.prototype.$singletonMessage
axios.interceptors.response.use(
res => res,
err => {
SingletonMessage.show({
type: 'error',
....
})
return Promise.reject(err)
}
)
这样我们多次接口报错,在当前Message销毁前,都会只有同一个实例,另外需要注意:上面具体实现的代码逻辑与单例模式uml规范会有些不同。学习设计模式重点在于思路,运用其思想解决业务中的问题才是关键。另外受限于element-ui本身,每次Message被销毁,其DOM节点也会被销毁。这其实也不是真正的单例模式。我们在这更多是借鉴其思路
这个交互在element-plus中得到了改善,新增了grouping选项,设置 grouping 为 true,内容相同的 message 将被合并
效果如下
ESM模块化
在ES6中, import引入模块就是一个很好的单例模式
// index.html
<script type="module">
import './single.js'
import './single.js'
</script>
// single.js
console.log('modeule test exec :>> ');
在上面代码中,import 引入的模块是会自动执行的,重复引入一个模块并不会执行多次。控制台只会打印一次
// index.html
<script type="module">
import obj from './single.js'
import obj2 from './single.js'
console.log(obj === obj2) // true
</script>
// single.js
const obj = {
a: 1
}
export default obj
正是基于单例模式。而ES6模块输出的是值的引用。因此上面代码我们可以发现obj与obj2引用的同一个对象
// index.html
<script type="module">
import obj1 from './single.js';
console.log(obj); // {a: 1}
setTimeout(() => {
console.log(obj); // {a: 2}
}, 3000);
</script>
// single.js
const obj = {
a: 1
}
setTimeout(() => {
obj.a = 2
}, 500);
export default obj
如果我们在当前模块修改了某个导出变量。别的模块再引入会是改变后(实时)的值。如果多个模块都引入该变量,则容易出现莫名问题且不好排查。因此我们尽量不要去改变导出变量的值
最后
单例模式本质核心点在于把之前生成的实例缓存起来。缓存的方式有很多种(类静态属性、闭包等),再对外提供一个访问该缓存实例的接口。由于js语言本身的灵活性且是单线程,js可以很方便的实现单例模式
到这里,就是本篇文章的全部内容了
如果你觉得该文章对你有帮助,欢迎大家点赞、关注和分享
如果你有疑问或者出入,评论区告诉我,我们一起讨论