设计模式-单例模式

374 阅读7分钟

title: 设计模式 - 单例模式 date: 2019-04-01 16:46:39 tags: 开始的第一天 category:

  • design

  最近看了下vue源码,真的是一脸的懵逼,把我看成哲学家,脑子里总是“我在哪” “我是谁”,“我在干啥” 哲学三连在回荡,听朋友推荐说先去看下设计模式,然后在看vue源码,会有拨开云雾见晴天的感觉。放弃了vue源码,准备看下设计模式。当然,是先看了最简单的单例模式。

单例模式

  什么是单例模式呢?保证一个类只有一个实例,并提供他的全局访问点。   单例模式是一种常用的模式,有一些对象我们往往只需要一个,比如线程池,全局缓存,浏览器的window对象等等。在javascript开发重中,单例模式用法也是非常的广泛。试想一下,当我们点击登陆按钮的时候,页面中会弹出一个弹窗,而这个弹窗是唯一的,无论点击多少次,弹窗总是被创建一次,那么这个弹窗就可以用单例模式来创建。当然有人说不用单例模式,当再次点击的时候,把之前的弹窗remove掉不就行了。在有些情况下,remove再添加,会有空白闪过,并且导致dom的回流与重绘。总体来说,还是使用单例模式比较好。

实现单例

  要实现一个单例模式并不复杂,无非是用一个变量来标记当前类是否已经创造过对象,如果是,则再下一次创建对象时,返回之前创造的对象。

 var singleTon = function(name) {
     this.name = name;
     this.instance = null;
 }
 singleTon.prototype.getName = function(){
     console.log(this.name)
 }
singleTon.getInstance = function(name){
    if(!this.instance) {
        this.instance = new singleTon(name)
    }
    return this.instance;
}

   或者

   var singleTon = function(name) {
       this.name = name;
   }
   singleTon.prototype.getName = function(){
       console.log(this.name)
   }
   singleTon.getInstance = (function(){
       var instance = null;
       return function(name){
           if(!instance) {
               instance = new sinleTon(name);
           }
           return instance;
       }
   })()

    上面的两种写法结果:    
   var a = singleTon.getInstance('first');
   var b = singleTon.getInstance('second');
   console.log(a === b) //true;
   a.getName() //first
   b.getName() //first

  我们通过singleTon.getInstance来获取singleTon类的唯一对象,这种方式相对简单。但有一个问题,就是增加了类的不透明性,singleTon类的使用者必须知道这是一个单例类,跟以往new XXX的方式来获取对象不同,这里偏要使用singleTon来获取对象。

透明的单例模式

   我们的目标是实现一个“透明”的单例类,用户用这个类创造对象的时候,可以像使用其他普通类一样。下面的例子,我们将使用createDiv单例类,它的作用是在页面中创建一个唯一的div;

    var CreateDiv = (function(){
        var instance = null;
        var CreateDiv = function(html) {
            if(instance) {
                return instance;

            }
            this.html = html;
            this.init()
            return instance = this;
        }
        CreateDiv.prototype.init = function(){
            var div = document.createElement('div');
            div.innerHTML = this.html;
            document.body.appendChild(div)
        }
        return createDiv
    })()

    var a = new CreateDiv('first');
    var b = new CreateDiv('second');
    console.log(a === b) //true

  虽然现在完成了一个透明的单例类,但是他同样有一些缺点。   为了把instance封装起来,使用了自执行的匿名函数和闭包,并且让这个闭包返回真正的SingleTon的构造方法,这就增加了程序的复杂度,阅读起来也是很不舒服。   观察现在的SingleTon构造函数。

    var CreateDiv = function(html) {
        if(instance) {
            return instance;
        }
        this.html = html;
        this.init();
        return instance = this;
    }

  在这段代码中,CreateDiv的构造函数实际上负责了两件事。第一是创建对象和执行初始化方法init(),第二是 保证只有一个对象。虽然,目前还没接触过单一职责原则得概念,但是可以明确的是,这是一种不好的做法,至少构造函数看起来很奇怪。   假设某天我们需要利用这个类,在页面中创建千千万万的div,即让这个类从单例类变成一个可以产生多个实例的类,那我们就必须得改写构造函数CreateDIv,把控制创建唯一对象的那一段给去掉,这种修改会给我增加不必要的麻烦。

用代理实现单例模式。

  我们通过引入代理的方式,解决上面提到的问题。   我们依然使用CreateDiv的构造函数,把负责管理单例的代码移除。

var CreateDiv = function(html){
    this.html = html;
    this.init()
}
CreateDiv.prototype.init = function(){
    var dom = document.createElement('div');
    div.innerHTML = this.html;
    dom.body.appendChild(div);
}
接下来引入代理类。proxySingleTonCreateDiv:
var proxySingleTonCreateDiv = function(){
    var instance;
    return function(html) {
        if(!instance){
            instance = new CreateDiv(html)
        }
        return instance
    }
}
var a = new proxySingleTonCreateDiv('first');
var b = new proxySingleTonCreateDiv('second')
console.log(a === b);

  通过引入代理类的方式,我们同样完成了一个单例模式的编写,跟之前不同的是,现在我们把负责管理单例模式的逻辑转移到了代理类proxySingleTonCreateDiv上。这样以来CreateDiv就变成了一个普通的类,它跟proxySingleTonCreateDiv组合起来就达到了单例类的模式。

惰性单例

  前面了解了一些单例的实现方式,本节来了解惰性单例。   惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种模式在实际开发中非常有作用。有用成都可能超过了我们的想象。实际上在本章的开始就有使用了这种技术,instance实例总是在我们调用singleTon.getInstance方法的时候才被创建,而不是在页面家在好的时候就创建,代码如下:

    singleTon.getInstance = (function(){
        var instance = null;
        return function(name) {
            if(!instance) {
                instance = new singleTon(name)
            }
            return instance;
        }
    })()

  假设我们是WebQQ的开发人员,点击中间的QQ头像时,会弹出一个登陆弹窗,很明显这个弹窗页面总是唯一的,不可能同时出现两个窗口。

webqq

  第一种是在页面初始化完成的时候,就建好了这个div,这个弹窗肯定是隐藏状态的,当用户点击登陆的时候,它才开始显示。

<html>
 <body>
       <button id="loginBtn">登陆</button>
 </body>
<script>
   var loginLayer = (function(){
       var div = document.createElement('div');
       div.innerHTML = '我是登陆弹窗';
       div.style.display = 'none';
       document.body.appendChild('div')
       return div
   })()
   documengt.getElementById('loginBtn').onclick = function(){
       loginLayer.style.display = 'block'
   }
</script>
</html>

  这种创建方式,会有一个问题。也许我们进去webQQ只是玩玩游戏或者看看天气,根本不需要进行登陆操作。但是登陆弹窗总是一开始就是被创建好的,那么很有可能白白浪费一些dom性能。

    <html>
        <body>
            <button id="loginBtn">登陆按钮</button>
            <script>
                var loginLayer = function(){
                    var dom = document.createElement('div');
                    dom.innerHTML = '我是登陆弹窗';
                    dom.style.display = 'none';
                    document.body.appendChild('dom');
                    return dom
                }
                document.getElementById('loginBtn').onclick = function(){
                    var createLayer = loginLayer();
                    createLayer.style.display = 'block';
                }
            </script>
        <body>
    </html>

  现在虽然实现的惰性的目的,但是失去了单例的效果。当我们点击登陆的时候,都会产生一个新的登陆弹窗。虽然我们可以在点击浮窗上的关闭按钮时(此处未实现)把这个浮 窗从页面中删除掉,但这样频繁地创建和删除节点明显是不合理的,频繁的引起dom的重绘与回流,也是不必要的。   这种情况,我门可以使用一个变量来判断是否已经创建过弹窗,这也是第一节中出现的做法。

    <html>
        <body>
        <button id="loginBtn">登陆按钮</button>
        <script>
          var loginLayer = (function(){
              var div = null;
              return function(html){
                  if(!div) {
                    div = document.createElement('div');
                    div.innerHTML = html;
                    div.style.display = "none";
                    document.body.appendChild(div);
                  }
                  return div
              }
          })();
          document.getElementById('loginBtn').onclick = function(){
              loginLayer('我是登陆弹窗').style.display = "block"
          }
        <script>
        </body>
        
     </html>

  如何判断dom是否发生了重绘或者回流呢,这里告诉大家一个小技巧。 首先打开chrome devTools,点击右上角的三点符号。 然后点击More tools,然后点击Rendering。

detool1
  将Paint flashing钩上。

detool2

  钩上后,第一次点击登陆按钮的时候,界面会出现绿色的方框,这就表示了dom发生了重绘与回流,再次点击,并不会出现绿框。

  单例模式是一种非常简单实用的模式,特别是惰性单例技术,在合适的的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在不同的方法中,这两个方法组合起来才是具有单例模式的威力。