【設(shè)計模式】工作中應(yīng)該如何使用單例模式

單例模式是非常常用的一種設(shè)計模式,工作中我們會用到例如彈窗、音頻管理、消息管理中心、公共工具函數(shù)類等在應(yīng)用中只需要單個實例的場景,這些都和單例模式密不可分。

image

單例模式,很常用也非常重要,將單例模式應(yīng)用于程序開發(fā)設(shè)計,可減少重復(fù)代碼,提升程序效率,同時單例的唯一性也使得數(shù)據(jù)流更加清晰,便于維護(hù)管理。

一、什么是單例模式

單例模式(Singleton Pattern)保證一個類只有一個實例,并提供一個訪問它的全局訪問點。

是不是瞬間想到了 JavaScript 中的全局變量 window、localStorage,它們在全局中提供了訪問點,并且只有唯一實例。

在一些多線程編程語言中,單例模式會涉及到同步鎖的問題,而 JavaScript 是單線程運行的,因此可以暫時忽略線程安全問題。

單例和單身狗類似,直到程序銷毀,整個程序里都找不出第二個和它能夠匹配上的

image

二、實現(xiàn)單例模式

單例模式從其定義就可以看出,是一個比較簡單的設(shè)計模式,其核心思想是保證唯一實例,因此如下簡單實現(xiàn)一個蒙層功能單例類,一步步完善。

2.1 蒙層單例類

現(xiàn)在的工作環(huán)境下都是基于 ES6 及以上的開發(fā)模式,因此我們直接帶入 class 思想去實現(xiàn),如果有需要了解相關(guān)基礎(chǔ)內(nèi)容的同學(xué)推薦閱讀:《這些JS設(shè)計模式的基礎(chǔ)知識點你都會了嗎?》。

蒙層的功能:

  1. 層級最大,覆蓋瀏覽器可視區(qū)域
  2. 阻止用戶所有點擊事件
  3. 透明蒙層

因此,該蒙層單例的實現(xiàn)如下:

/**
 * Mask 蒙層單例
 */
class Mask {
  static instance: Mask;
  private isShow: boolean;
  private maskDom: HTMLDivElement;

  static getInstance() {
    if (!Mask.instance) {
      Mask.instance = new Mask();
    }
    return Mask.instance;
  }

  constructor() {
    this.isShow = false;
    this.maskDom = this.init();
  }

  /**
   * 創(chuàng)建蒙層DOM
   */
  private init() {
    const dom = document.createElement("div");
    dom.setAttribute(
      "style",
      "z-index: 99999; position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: none; pointer-events: all; user-select: none; cursor: not-allowed;"
    );
    document.body.appendChild(dom);
    return dom;
  }

  /**
   * show 顯示蒙層
   */
  public show() {
    if (this.isShow) return;
    this.maskDom.style["display"] = "block";
    this.isShow = true;
  }

  /**
   * hide 隱藏蒙層
   */
  public hide() {
    if (!this.isShow) return;
    this.maskDom.style["display"] = "none";
    this.isShow = false;
  }
}

// 直接導(dǎo)出實例
export default Mask.getInstance();

使用方式:

import Mask from "./utils/Mask";

Mask.show();
Mask.hide();

這種在已開始就創(chuàng)建實例的方式,被稱作“餓漢式單例”,另一種在需要的時候才創(chuàng)建實例的方式被稱作“懶漢式單例”。

因此“餓漢式單例”的缺點就是:類加載時就初始化,浪費內(nèi)存。

不過在現(xiàn)代借助 Webpack 等打包構(gòu)建工具,如果沒有使用到這個組件,也不會將這個組件打包進(jìn)來,另外在 React、Vue 框架按需加載組件的設(shè)計實現(xiàn)下,組件也是按需通過網(wǎng)絡(luò)下載分包組件文件,然后緩存起來,所以浪費內(nèi)存這一缺點可忽略。因此,在 JavaScript 中懶漢式和餓漢式的區(qū)分不大。

推薦使用導(dǎo)出的時候就導(dǎo)出一個實例,例如:

export default Mask.getInstance();

2.2 透明蒙層單例

在上述的導(dǎo)出實例中,只能調(diào)用 Mask.getInstance() ,對于其他使用者來說其實已經(jīng)規(guī)避了如何判斷是否是單例類的問題。

另外,如果我們的導(dǎo)出形式是這樣的:

export default Mask;

那么其他同學(xué)在引用這個組件時候,如果不知道是一個單例類,那他就可能會直接 new Mask() 方式使用,那么就會創(chuàng)建多個不同的實例,失去了單例的效果,為了讓單例類使用的時候能像普通類創(chuàng)建對象的寫法一致,在使用的時候和其他普通類一樣,我們把這個處理方式叫做“透明化”。

為此,稍微改動下構(gòu)造函數(shù)中的判斷:

constructor() {
  if (Mask.instance) {
    return Mask.instance;
  }
  this.isShow = false;
  this.maskDom = this.init();
  return Mask.instance = this;
}

...

export default Mask;

使用驗證:

const a = new Mask();
a.show();
const b = new Mask();
b.hide();
console.log("是否相等:", a === b);
// output: 是否相等 true,并且蒙層被隱藏
image

如果為了保證類的透明性,使用方式的統(tǒng)一,可以采用在構(gòu)造函數(shù)中預(yù)先判斷是否存在實例的方式來實現(xiàn)。

2.3 單例化工具函數(shù)

通過上述對單例模式的實現(xiàn)和使用,其實可以想到一個問題,單例模式,只需要保證唯一實例即可,而保證唯一實例的方式,是通過一個變量來判斷當(dāng)前實例是否已經(jīng)被創(chuàng)建過,如果已經(jīng)創(chuàng)建了,則直接返回該實例,否則創(chuàng)建后再返回實例。

那么通用的單例模式,應(yīng)該將單例化和類的職責(zé)拆分開。

講到這,對于前面分享的《從“圖片預(yù)加載”認(rèn)識代理設(shè)計模式》中的緩存代理就非常相似了,借助代理模式的思想,用閉包來緩存單例。

單例化緩存工具函數(shù):

export const proxySingleton = (fn) => {
  let instance;
  return () => {
    if (instance) return instance;
    return instance = new fn();
  };
};

傳入我們之前寫的任意類,類名指向了構(gòu)造函數(shù)

export const singletonMask = proxySingleton(Mask);

使用的時候:

import { singletonMask } from "./utils/ProxySingleton";

const a = singletonMask();
a.show();
const b = singletonMask();
b.hide();
console.log("是否相等:", a === b);
// output: 是否相等 true,并且蒙層被隱藏

這樣我們就將單例化和創(chuàng)建蒙層類職責(zé)拆分,遵循單一職責(zé)原則。

2.4 ES6 export實例 import單例

舉一反三,是不是在 ES6 中直接 export 一個實例是否就可以看作是單例了吶?

export default new Mask();

import 引入是采用的單例(Singleton)模式,多次用 import 引入同一個模塊時,只會引入一次該模塊的實例 —— 《ECMAScript 6 入門

因此,如果要通用的單例對象,我們可以直接在export的時候,就導(dǎo)出實例。

之前我們研究過 EventEmitter3 事件觸發(fā)器的原理,為了讓之在全局只有一個實例,我們的使用方法如下:

// ./utils/eventEmitter.ts
import EventEmitter from 'eventemitter3';

export default new EventEmitter();

這樣導(dǎo)出的 eventEmitter 就是一個單例,唯一,且全局可訪問。

三、總結(jié)

單例模式在工作中經(jīng)常用到,當(dāng)我們有意識地使用單例管理具有唯一屬性的實例,將會使得程序更容易管理維護(hù)。

結(jié)合 ES6 的 import 和 export 關(guān)鍵詞,單例模式的應(yīng)用也變得更加簡便。

在實現(xiàn)單例中,我們有將單例和蒙層類功能拆分開,也有合在一起的,這取決于在你項目中想要如何設(shè)計,如果單例并非是大面積的組件套用,其實還是推薦合在一起,有助于后續(xù)在單文件中維護(hù)整個功能類。

歡迎關(guān)注公眾號學(xué)習(xí)前端/安全/ML:ITDYBOY

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容