單列模式

單例模式

單例模式多是設計模式裏面最簡單的模式了,雖然簡單,但在咱們平常生活和編程中卻常常接觸到,本節咱們一塊兒來學習一下。
單例模式 (Singleton Pattern)又稱爲單體模式,保證一個類只有一個實例,並提供一個訪問它的全局訪問點。也就是說,第二次使用同一個類建立新對象的時候,應該獲得與第一次建立的對象徹底相同的對象。

1.你曾經碰見過的單例模式

  • 當咱們在電腦上玩經營類的遊戲,通過一番眼花繚亂的騷操做好不容易走上正軌,夜深了咱們去休息,次日打開電腦,發現要從頭玩,立馬就把電腦扔窗外了,因此通常但願從前一天的進度接着打,這裏就用到了存檔。每次玩這遊戲的時候,咱們都但願拿到同一個存檔接着玩,這就是屬於單例模式的一個實例。
  • 編程中也有不少對象咱們只須要惟一一個,好比數據庫鏈接、線程池、配置文件緩存、瀏覽器中的 window/document 等,若是建立多個實例,會帶來資源耗費嚴重,或訪問行爲不一致等狀況。
  • 相似於數據庫鏈接實例,咱們可能頻繁使用,可是建立它所須要的開銷又比較大,這時只使用一個數據庫鏈接就能夠節約不少開銷。一些文件的讀取場景也相似,若是文件比較大,那麼文件讀取就是一個比較重的操做。好比這個文件是一個配置文件,那麼徹底能夠將讀取到的文件內容緩存一份,每次來讀取的時候訪問緩存便可,這樣也能夠達到節約開銷的目的。
在相似場景中,這些例子有如下特色:
  • 每次訪問者來訪問,返回的都是同一個實例;
  • 若是一開始實例沒有建立,那麼這個特定類須要自行建立這個實例;

2. 實例的代碼實現

  • 若是你是一個前端er,那麼你確定知道瀏覽器中的 window 和 document 全局變量,這兩個對象都是單例,任什麼時候候訪問他們都是同樣的對象,window 表示包含 DOM 文檔的窗口,document 是窗口中載入的 DOM 文檔,分別提供了各自相關的方法。
  • 在 ES6 新增語法的 Module 模塊特性,經過 import/export 導出模塊中的變量是單例的,也就是說,若是在某個地方改變了模塊內部變量的值,別的地方再引用的這個值是改變以後的。除此以外,項目中的全局狀態管理模式 Vuex、Redux、MobX 等維護的全局狀態,vue-router、react-router 等維護的路由實例,在單頁應用的單頁面中都屬於單例的應用(但不屬於單例模式的應用。
  • 在 JavaScript 中使用字面量方式建立一個新對象時,實際上沒有其餘對象與其相似,由於新對象已是單例了:
  • 那麼問題來了,如何對構造函數使用 new 操做符建立多個對象時,僅獲取同一個單例對象呢。
  • 對於剛剛打經營遊戲的例子,咱們能夠用 JavaScript 來
    實現一下:
function ManageGame(){
      if(ManageGame._schedule){  // 判斷是否已經有單例了
            return ManageGame._schedule
      }
      ManageGame._schedule = this
  }

  ManageGame.getInstance = function(){
      if(ManageGame._schedule){  // 判斷是否已經有單例了
            return ManageGame._schedule
      }
      return ManageGame ._schedule =new ManageGame()
  }

  const schedule1 = new ManageGame()
  const schedule2 =ManageGame.getInstance()

  console.log(schedule1===schedule2)
ts的 class 改造
class ManageGame{
        private static schedule: any = null;
        static getInstance() {
            if (ManageGame.schedule) {        // 判斷是否已經有單例了
                return ManageGame.schedule
            }
            return ManageGame.schedule = new ManageGame()
        }
        constructor() {
            if (ManageGame.schedule) {        // 判斷是否已經有單例了
                return ManageGame.schedule
            }
            ManageGame.schedule = this
        }
    }
    const schedule1 = new ManageGame()
    const schedule2 = ManageGame.getInstance()
    console.log(schedule1 === schedule2)// true
缺點:上面方法的缺點在於維護的實例做爲靜態屬性直接暴露,外部能夠直接修改。

3. 單例模式的通用實現

根據上面的例子提煉一下單例模式,遊戲能夠被認爲是一個特定的類(Singleton),而存檔是單例(instance),每次訪問特定類的時候,都會拿到同一個實例。主要有下面幾個概念:
  • Singleton :特定類,這是咱們須要訪問的類,訪問者要拿到的是它的實例;
  • instance :單例,是特定類的實例,特定類通常會提供 getInstance 方法來獲取該單例;
  • getInstance :獲取單例的方法,或者直接由 new 操做符獲取;

3.1 IIFE方式建立單列模式

  • 簡單實現中,咱們提到了缺點是實例會暴露,那麼這裏咱們首先使用當即調用函數 IIFE 將不但願公開的單例實例 instance 隱藏。
  • 固然也可使用構造函數複寫將閉包進行的更完全
const Singleton = (function() {
    let _instance = null        // 存儲單例
    
    const Singleton = function() {
        if (_instance) return _instance     // 判斷是否已有單例
        _instance = this
        this.init()                         // 初始化操做
        return _instance
    }
    
    Singleton.prototype.init = function() {
        this.foo = 'Singleton Pattern'
    }
    
    return Singleton
})()

const visitor1 = new Singleton()
const visitor2 = new Singleton()

console.log(visitor1 === visitor2)    // true
  • 這樣一來,雖然仍使用一個變量 _instance 來保存單例,可是因爲在閉包的內部,因此外部代碼沒法直接修改。
  • 在這個基礎上,咱們能夠繼續改進,增長 getInstance 靜態方法:
const Singleton = (function() {
    let _instance = null        // 存儲單例
    
    const Singleton = function() {
        if (_instance) return _instance     // 判斷是否已有單例
        _instance = this
        this.init()                         // 初始化操做
        return _instance
    }
    
    Singleton.prototype.init = function() {
        this.foo = 'Singleton Pattern'
    }
    
    Singleton.getInstance = function() {
        if (_instance) return _instance
        _instance = new Singleton()
        return _instance
    }
    
    return Singleton
})()

const visitor1 = new Singleton()
const visitor2 = new Singleton()         // 既能夠 new 獲取單例
const visitor3 = Singleton.getInstance() // 也能夠 getInstance 獲取單例

console.log(visitor1 === visitor2)    // true
console.log(visitor1 === visitor3)    // true
  • 代價和上例同樣是閉包開銷,而且由於 IIFE 操做帶來了額外的複雜度,讓可讀性變差。
  • IIFE 內部返回的 Singleton 纔是咱們真正須要的單例的構造函數,外部的 Singleton 把它和一些單例模式的建立邏輯進行了一些封裝。
  • IIFE 方式除了直接返回一個方法/類實例以外,還能夠經過模塊模式的方式來進行,就不貼代碼了,代碼實如今 Github 倉庫中,讀者能夠本身瞅瞅。

3.2 塊級做用域方式建立單例

let getInstance

{
    let _instance = null        // 存儲單例
    
    const Singleton = function() {
        if (_instance) return _instance     // 判斷是否已有單例
        _instance = this
        this.init()                         // 初始化操做
        return _instance
    }
    
    Singleton.prototype.init = function() {
        this.foo = 'Singleton Pattern'
    }
    
    getInstance = function() {
        if (_instance) return _instance
        _instance = new Singleton()
        return _instance
    }
}

const visitor1 = getInstance()
const visitor2 = getInstance()

console.log(visitor1 === visitor2)

3.3 單例模式賦能

以前的例子中,單例模式的建立邏輯和原先這個類的一些功能邏輯(好比 init 等操做)混雜在一塊兒,根據單一職責原則,這個例子咱們還能夠繼續改進一下,將單例模式的建立邏輯和特定類的功能邏輯拆開,這樣功能邏輯就能夠和正常的類同樣。
/* 功能類 */
class FuncClass {
    constructor(bar) { 
        this.bar = bar
        this.init()
    }
    
    init() {
        this.foo = 'Singleton Pattern'
    }
}

/* 單例模式的賦能類 */
const Singleton = (function() {
    let _instance = null        // 存儲單例
    
    const ProxySingleton = function(bar) {
        if (_instance) return _instance     // 判斷是否已有單例
        _instance = new FuncClass(bar)
        return _instance
    }
    
    ProxySingleton.getInstance = function(bar) {
        if (_instance) return _instance
        _instance = new Singleton(bar)
        return _instance
    }
    
    return ProxySingleton
})()

const visitor1 = new Singleton('單例1')
const visitor2 = new Singleton('單例2')
const visitor3 = Singleton.getInstance()

console.log(visitor1 === visitor2)    // true
console.log(visitor1 === visitor3)    // true
  • 這樣的單例模式賦能類也可被稱爲代理類,將業務類和單例模式的邏輯解耦,把單例的建立邏輯抽象封裝出來,有利於業務類的擴展和維護。代理的概念咱們將在後面代理模式的章節中更加詳細地探討。
  • 使用相似的概念,配合 ES6 引入的 Proxy 來攔截默認的 new 方式,咱們能夠寫出更簡化的單例模式賦能方法:
/* Person 類 */
class Person {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
}

/* 單例模式的賦能方法 */
function Singleton(FuncClass) {
    let _instance
    return new Proxy(FuncClass, {
        construct(target, args) {
            return _instance || (_instance = Reflect.construct(FuncClass, args)) // 使用 new FuncClass(...args) 也能夠
        }
    })
}

const PersonInstance = Singleton(Person)

const person1 = new PersonInstance('張小帥', 25)
const person2 = new PersonInstance('李小美', 23)

console.log(person1 === person2)    // true

4. 惰性單例、懶漢式-餓漢式

有時候一個實例化過程比較耗費性能的類,可是卻一直用不到,若是一開始就對這個類進行實例化就顯得有些浪費,那麼這時咱們就可使用惰性建立,即延遲建立該類的單例。以前的例子都屬於惰性單例,實例的建立都是 new 的時候才進行。前端

惰性單例又被成爲懶漢式,相對應的概念是餓漢式:vue

  • 懶漢式單例是在使用時才實例化
  • 餓漢式是當程序啓動時或單例模式類一加載的時候就被建立。
  • 咱們能夠舉一個簡單的例子比較一下:
class FuncClass {
    constructor() { this.bar = 'bar' }
}

// 餓漢式
const HungrySingleton = (function() {
    const _instance = new FuncClass()
    
    return function() {
        return _instance
    }
})()

// 懶漢式
const LazySingleton = (function() {
    let _instance = null
    
    return function() {
        return _instance || (_instance = new FuncClass())
    }
})()

const visitor1 = new HungrySingleton()
const visitor2 = new HungrySingleton()
const visitor3 = new LazySingleton()
const visitor4 = new LazySingleton()

console.log(visitor1 === visitor2)    // true
console.log(visitor3 === visitor4)    // true
ts實現
  • 懶漢式單例react

    class LazySingleton{
     private static instance:LazySingleton = null;
     private constructor(){
         //private 避免類在外部被實例化
     }
     public static  getInstance():LazySingleton{
          if (LazySingleton.instance == null) {
             LazySingleton.instance = new LazySingleton();
       }
       return LazySingleton.instance;
     }
      someMethod() {}
    }
    
    let someThing = new LazySingleton(); // Error: constructor of 'singleton' is private
    
    let instacne = LazySingleton.getInstance(); // do some thing with the instance
用懶漢式單例模式模擬產生美國當今總統對象。
分析:在每一屆任期內,美國的總統只有一人,因此本實例適合用單例模式實現,圖 2 所示是用懶漢式單例實現的結構圖。
class SingletonLazy{
        public static main(arg) {
            let zt1 = President.getInstance();
            zt1.getName();
            let zt2 = President.getInstance();
            zt2.getName();
            if (zt1 === zt2) {
                console.log("他們是同一我的");
            } else {
                console.log("他們不是同一人");
            }
        }
    }

    class President{
        private static instance: President = null;
        private constructor() {
            console.log("產生一個總統了");
        }
        public static  getInstance():President{
            if (President.instance == null) {
                President.instance = new President();
            } else {
                console.log("已經有了一個總統了,不能產生新總統!");
            }
            return President.instance;
        }
        public  getName():void {
          console.log("我是美國總統:特朗普。");
        }
        
    }
  • 餓漢式單例
namespace 餓漢式{
    class HungrySingleton{
        private static instance: HungrySingleton = new HungrySingleton();
        private constructor() {
            
        }
        public static getInstance(): HungrySingleton{
            return HungrySingleton.instance;
        }
    }
    
    let someThing = new HungrySingleton(); // Error: constructor of 'singleton' is private
    
    let instacne = HungrySingleton.getInstance(); // do some thing with the instance
}

5. 源碼中的單例模式

以 ElementUI 爲例,ElementUI 中的全屏 Loading 蒙層調用有兩種形式:
// 1. 指令形式
Vue.use(Loading.directive)
// 2. 服務形式
Vue.prototype.$loading = service

用服務方式使用全屏 Loading 是單例的,即在前一個全屏 Loading 關閉前再次調用全屏 Loading,並不會建立一個新的 Loading 實例,而是返回現有全屏 Loading 的實例。vue-router

下面咱們能夠看看 ElementUI 2.9.2 的源碼是如何實現的,爲了觀看方便,省略了部分代碼:
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

6. 單例模式的優缺點

單列模式的特色
  • 單例類只有一個實例對象;
  • 該單例對象必須由單例類自行建立;
  • 單例類對外提供一個訪問該單例的全局訪問點。
單例模式主要解決的問題就是節約資源,保持訪問一致性。
簡單分析一下它的優勢:
  • 單例模式在建立後在內存中只存在一個實例,節約了內存開支和實例化時的性能開支,特別是須要重複使用一個建立開銷比較大的類時,比起實例不斷地銷燬和從新實例化,單例能節約更多資源,好比數據庫鏈接;
  • 單例模式能夠解決對資源的多重佔用,好比寫文件操做時,由於只有一個實例,能夠避免對一個文件進行同時操做;
    *只使用一個實例,也能夠減少垃圾回收機制 GC(Garbage Collecation) 的壓力,表如今瀏覽器中就是系統卡頓減小,操做更流暢,CPU 資源佔用更少;
單例模式也是有缺點的
  • 單例模式對擴展不友好,通常不容易擴展,由於單例模式通常自行實例化,沒有接口
  • 在併發測試中,單例模式不利於代碼調試。在調試過程當中,若是單例中的代碼沒有執行完,也不能模擬生成一個新的對象。
  • 與單一職責原則衝突,一個類應該只關心內部邏輯,而不關心外面怎麼樣來實例化;
單例模式的使用場景那咱們應該在什麼場景下使用單例模式呢:
  • 當一個類的實例化過程消耗的資源過多,可使用單例模式來避免性能浪費;
  • 當項目中須要一個公共的狀態,那麼須要使用單例模式來保證訪問一致性
相關文章
相關標籤/搜索