定义
单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
生活中例子
当我们在电脑上玩经营类的游戏,经过一番眼花缭乱的骚操作好不容易走上正轨,夜深了我们去休息,第二天打开电脑,发现要从头玩,立马就把电脑扔窗外了,所以一般希望从前一天的进度接着打,这里就用到了存档。每次玩这游戏的时候,我们都希望拿到同一个存档接着玩,这就是属于单例模式的一个实例。
使用的场景
有一些对象我们往往只需要一个,比如线程池、全局缓存、浏览器中的 window 对象等。
实现单例模式
要实现一个标准的单例模式并不复杂,无非是用一个变量来标志当前是否已经为某个类创建过对象,如果是,则在下一次获取该类的实例时,直接返回之前创建的对象。
{a:1}==={a:1} //false
那么问题来了,如何对构造函数使用 new 操作符创建多个对象时,仅获取同一个单例对象呢。
var Singleton = function (name) {
this.name = name
}
Singleton.getInstance = (function (name) {
var instance = null
return function (name) {
if (!instance) {
instance = new Singleton(name)
}
return instance
}
})(name)
var a = Singleton.getInstance('张三')
var b = Singleton.getInstance('李四')
console.log(a, b, a === b);
这个构造函数在内部维护(或者直接挂载自己身上)一个实例,第一次执行 new 的时候判断这个实例有没有创建过,创建过就直接返回,否则走创建流程。
透明的单例模式
虽然现在已经完成了一个单例模式的编写,但这段单例模式代码的意义并不大。从下一节开
始,我们将一步步编写出更好的单例模式。
我们现在的目标是实现一个“透明”的单例类,用户从这个类中创建对象的时候,可以像使
用其他任何普通类一样。
var CreateDiv = (function () {
var instance = null
var CreateDiv = function (text) {
if (instance) {
return instance
}
this.text = text
this.init()
return instance = this
}
CreateDiv.prototype.init = function () {
var div = document.createElement('div');
div.innerHTML = this.text;
document.body.appendChild(div);
}
return CreateDiv
})();
var a = new CreateDiv('我建议滑着走');
var b = new CreateDiv('sven2');
console.log(a, b, a === b);
在这段代码中,CreateDiv 的构造函数实际上负责了两件事情。第一是创建对象和执行初始 化 init 方法,第二是保证只有一个对象。
假设我们某天需要利用这个类,在页面中创建千千万万的 div,即要让这个类从单例类变成
一个普通的可产生多个实例的类,那我们必须得改写 CreateDiv 构造函数,把控制创建唯一对象
的那一段去掉,这种修改会给我们带来不必要的烦恼。
代理单例模式
var CreateDiv = function (html) {
this.html = html
this.init()
}
CreateDiv.prototype.init = function () {
var div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
// 代理类
var ProxySingletonCreateDiv = (function () {
var instance
return function (html) {
if (!instance) {
instance = new CreateDiv(html)
}
return instance
}
})();
var a = new ProxySingletonCreateDiv('我建议滑着走');
var b = new ProxySingletonCreateDiv('sven2');
console.log(a, b, a === b);
惰性单例模式
惰性单例指的是在需要的时候才创建对象实例。
问题场景:先有一个按钮,点击后会出现一个弹窗
第一种解决方案是在页面加载完成的时候便创建好这个 div 浮窗,这个浮窗一开始肯定是隐藏状态的,当用户点击登录按钮的时候,它才开始显示。
var loginLayer = (function(){ var div = document.createElement('div') div.innerHTML = '我是登录浮窗' div.style.display = 'none' document.body.appendChild(div) return div })(); document.getElementById('loginBtn').onclick = function(){ loginLayer.style.display = 'block'; }它的缺点:用户也许不需要点击该按钮,则平白无故浪费一个dom节点。
var loginLayer = function () { var div = document.createElement("div") div.innerHTML = "点击chuangjian" div.style.display = 'none' document.body.appendChild(div) return div } document.getElementById('loginBtn').onclick = function () { var creatDiv = loginLayer() creatDiv.style.display = 'block'; }虽然现在达到了惰性的目的,但失去了单例的效果。当我们每次点击登录按钮的时候,都会 创建一个新的登录浮窗 div。虽然我们可以在点击浮窗上的关闭按钮时(此处未实现)把这个浮
窗从页面中删除掉,但这样频繁地创建和删除节点明显是不合理的,也是不必要的。var createLoginLayer = (function (params) { var div if (!div) { var div = document.createElement('div') div.innerHTML = '我是登录浮窗' div.style.display = 'none' document.body.appendChild(div) return div } return div })(); document.getElementById('loginBtn').onclick = function () { var creatDiv = createLoginLayer creatDiv.style.display = 'block'; } ```
通用的单例模式
这段代码仍然是违反单一职责原则的,创建对象和管理单例的逻辑都放在 createLoginLayer 对象内部。
如果我们下次需要创建页面中唯一的 iframe,或者 script 标签,用来跨域请求数据,就
必须得如法炮制,把 createLoginLayer 函数几乎照抄一遍。
var createIframe= (function(){
var iframe;
return function(){
if ( !iframe){
iframe= document.createElement( 'iframe' );
iframe.style.display = 'none';
document.body.appendChild( iframe);
}
return iframe;
}
})();
我们需要把不变的部分隔离出来,先不考虑创建一个 div 和创建一个 iframe 有多少差异,管理单例的逻辑其实是完全可以抽象出来的,这个逻辑始终是一样的:用一个变量来标志是否创建过对象,如果是,则在下次直接返回这个已经创建好的对象。
var getSingle = function( fn ){
var result;
return function(){
return result || ( result = fn .apply(this, arguments ) );
}
};
接下来将用于创建登录浮窗的方法用参数 fn 的形式传入 getSingle,我们不仅可以createLoginLayer,还能传入 createScript、createIframe、createXhr 等。之后再让 getSingle 返回一个新的函数,并且用一个变量 result 来保存 fn 的计算结果。
var createLoginLayer = function () {
var div = document.createElement('div')
div.innerHTML = '我就是弹窗'
document.body.appendChild(div)
return div
}
- 完整的代码
var getSingle = function (fn) {
var instance
return function () {
return instance || (instance = fn.apply(this, arguments))
}
}
var createLoginLayer = function () {
var div = document.createElement('div')
div.innerHTML = '我就是弹窗'
document.body.appendChild(div)
return div
}
var createSingleLoginLayer = new getSingle(createLoginLayer)
document.getElementById('loginBtn').onclick = function () {
var loginLayer = createSingleLoginLayer();
loginLayer.style.display = 'block';
};
单例模式的优缺点
优点
- 单例模式在创建后在内存中只存在一个实例,节约了内存开支和实例化时的性能开支,特别是需要重复使用一个创建开销比较大的类时,比起实例不断地销毁和重新实例化,单例能节约更多资源,比如数据库连接;
- 单例模式可以解决对资源的多重占用,比如写文件操作时,因为只有一个实例,可以避免对一个文件进行同时操作;
- 只使用一个实例,也可以减小垃圾回收机制 GC(Garbage Collecation) 的压力,表现在浏览器中就是系统卡顿减少,操作更流畅,CPU 资源占用更少;
缺点
- 单例模式对扩展不友好,一般不容易扩展,因为单例模式一般自行实例化,没有接口;
- 与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化;
使用场景
- 当一个类的实例化过程消耗的资源过多,可以使用单例模式来避免性能浪费;
- 当项目中需要一个公共的状态,那么需要使用单例模式来保证访问一致性;
源码中的单例
以 ElementUI 为例,ElementUI 中的全屏 Loading 蒙层调用有两种形式:
- // 1. 指令形式
Vue.use(Loading.directive) - // 2. 服务形式
Vue.prototype.$loading = service
import Vue from 'vue'
import loadingVue from './loading.vue'
const LoadingConstructor = Vue.extend(loadingVue)
let fullscreenLoading
const Loading = (options = {}) => {
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
})
if (options.fullscreen) {
fullscreenLoading = instance
}
return instance
}
export default Loading
这里的单例是 fullscreenLoading,是存放在闭包中的,如果用户传的 options 的 fullscreen 为 true 且已经创建了单例的情况下则回直接返回之前创建的单例,如果之前没有创建过,则创建单例并赋值给闭包中的 fullscreenLoading 后返回新创建的单例实例。
这是一个典型的单例模式的应用,通过复用之前创建的全屏蒙层单例,不仅减少了实例化过程,而且避免了蒙层叠加蒙层出现的底色变深的情况。