設(shè)計(jì)模式 | 工作中會(huì)用到的單例模式
本文轉(zhuǎn)載自微信公眾號(hào)「DYBOY」,作者DYBOY。轉(zhuǎn)載本文請(qǐng)聯(lián)系DYBOY公眾號(hào)。
單例模式是非常常用的一種設(shè)計(jì)模式,工作中我們會(huì)用到例如彈窗、音頻管理、消息管理中心、公共工具函數(shù)類等在應(yīng)用中只需要單個(gè)實(shí)例的場(chǎng)景,這些都和單例模式密不可分。
單例模式,很常用也非常重要,將單例模式應(yīng)用于程序開發(fā)設(shè)計(jì),可減少重復(fù)代碼,提升程序效率,同時(shí)單例的唯一性也使得數(shù)據(jù)流更加清晰,便于維護(hù)管理。
一、什么是單例模式
單例模式(Singleton Pattern)保證一個(gè)類只有一個(gè)實(shí)例,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)。
是不是瞬間想到了 JavaScript 中的全局變量 window、localStorage,它們?cè)谌种刑峁┝嗽L問(wèn)點(diǎn),并且只有唯一實(shí)例。
在一些多線程編程語(yǔ)言中,單例模式會(huì)涉及到同步鎖的問(wèn)題,而 JavaScript 是單線程運(yùn)行的,因此可以暫時(shí)忽略線程安全問(wèn)題。
單例和單身狗類似,直到程序銷毀,整個(gè)程序里都找不出第二個(gè)和它能夠匹配上的
二、實(shí)現(xiàn)單例模式
單例模式從其定義就可以看出,是一個(gè)比較簡(jiǎn)單的設(shè)計(jì)模式,其核心思想是保證唯一實(shí)例,因此如下簡(jiǎn)單實(shí)現(xiàn)一個(gè)蒙層功能單例類,一步步完善。
2.1 蒙層單例類
現(xiàn)在的工作環(huán)境下都是基于 ES6 及以上的開發(fā)模式,因此我們直接帶入 class 思想去實(shí)現(xiàn),如果有需要了解相關(guān)基礎(chǔ)內(nèi)容的同學(xué)推薦閱讀:《這些JS設(shè)計(jì)模式的基礎(chǔ)知識(shí)點(diǎn)你都會(huì)了嗎?》。
蒙層的功能:
- 層級(jí)最大,覆蓋瀏覽器可視區(qū)域
- 阻止用戶所有點(diǎn)擊事件
- 透明蒙層
因此,該蒙層單例的實(shí)現(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)出實(shí)例
- export default Mask.getInstance();
使用方式:
- import Mask from "./utils/Mask";
- Mask.show();
- Mask.hide();
這種在已開始就創(chuàng)建實(shí)例的方式,被稱作“餓漢式單例”,另一種在需要的時(shí)候才創(chuàng)建實(shí)例的方式被稱作“懶漢式單例”。
因此“餓漢式單例”的缺點(diǎn)就是:類加載時(shí)就初始化,浪費(fèi)內(nèi)存。
不過(guò)在現(xiàn)代借助 Webpack 等打包構(gòu)建工具,如果沒有使用到這個(gè)組件,也不會(huì)將這個(gè)組件打包進(jìn)來(lái),另外在 React、Vue 框架按需加載組件的設(shè)計(jì)實(shí)現(xiàn)下,組件也是按需通過(guò)網(wǎng)絡(luò)下載分包組件文件,然后緩存起來(lái),所以浪費(fèi)內(nèi)存這一缺點(diǎn)可忽略。因此,在 JavaScript 中懶漢式和餓漢式的區(qū)分不大。
推薦使用導(dǎo)出的時(shí)候就導(dǎo)出一個(gè)實(shí)例,例如:
- export default Mask.getInstance();
2.2 透明蒙層單例
在上述的導(dǎo)出實(shí)例中,只能調(diào)用 Mask.getInstance() ,對(duì)于其他使用者來(lái)說(shuō)其實(shí)已經(jīng)規(guī)避了如何判斷是否是單例類的問(wèn)題。
另外,如果我們的導(dǎo)出形式是這樣的:
- export default Mask;
那么其他同學(xué)在引用這個(gè)組件時(shí)候,如果不知道是一個(gè)單例類,那他就可能會(huì)直接 new Mask() 方式使用,那么就會(huì)創(chuàng)建多個(gè)不同的實(shí)例,失去了單例的效果,為了讓單例類使用的時(shí)候能像普通類創(chuàng)建對(duì)象的寫法一致,在使用的時(shí)候和其他普通類一樣,我們把這個(gè)處理方式叫做“透明化”。
為此,稍微改動(dò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;
使用驗(yàn)證:
- const a = new Mask();
- a.show();
- const b = new Mask();
- b.hide();
- console.log("是否相等:", a === b);
- // output: 是否相等 true,并且蒙層被隱藏
如果為了保證類的透明性,使用方式的統(tǒng)一,可以采用在構(gòu)造函數(shù)中預(yù)先判斷是否存在實(shí)例的方式來(lái)實(shí)現(xiàn)。
2.3 單例化工具函數(shù)
通過(guò)上述對(duì)單例模式的實(shí)現(xiàn)和使用,其實(shí)可以想到一個(gè)問(wèn)題,單例模式,只需要保證唯一實(shí)例即可,而保證唯一實(shí)例的方式,是通過(guò)一個(gè)變量來(lái)判斷當(dāng)前實(shí)例是否已經(jīng)被創(chuàng)建過(guò),如果已經(jīng)創(chuàng)建了,則直接返回該實(shí)例,否則創(chuàng)建后再返回實(shí)例。
那么通用的單例模式,應(yīng)該將單例化和類的職責(zé)拆分開。
講到這,對(duì)于前面分享的《從“圖片預(yù)加載”認(rèn)識(shí)代理設(shè)計(jì)模式》中的緩存代理就非常相似了,借助代理模式的思想,用閉包來(lái)緩存單例。
單例化緩存工具函數(shù):
- export const proxySingleton = (fn) => {
- let instance;
- return () => {
- if (instance) return instance;
- return instance = new fn();
- };
- };
傳入我們之前寫的任意類,類名指向了構(gòu)造函數(shù)
- export const singletonMask = proxySingleton(Mask);
使用的時(shí)候:
- 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實(shí)例 import單例
舉一反三,是不是在 ES6 中直接 export 一個(gè)實(shí)例是否就可以看作是單例了吶?
- export default new Mask();
import 引入是采用的單例(Singleton)模式,多次用 import 引入同一個(gè)模塊時(shí),只會(huì)引入一次該模塊的實(shí)例 —— 《ECMAScript 6 入門》
因此,如果要通用的單例對(duì)象,我們可以直接在 export 的時(shí)候,就導(dǎo)出實(shí)例。
之前我們研究過(guò) EventEmitter3 事件觸發(fā)器的原理,為了讓之在全局只有一個(gè)實(shí)例,我們的使用方法如下:
- // ./utils/eventEmitter.ts
- import EventEmitter from 'eventemitter3';
- export default new EventEmitter();
這樣導(dǎo)出的 eventEmitter 就是一個(gè)單例,唯一,且全局可訪問(wèn)。
三、總結(jié)
單例模式在工作中經(jīng)常用到,當(dāng)我們有意識(shí)地使用單例管理具有唯一屬性的實(shí)例,將會(huì)使得程序更容易管理維護(hù)。
結(jié)合 ES6 的 import 和 export 關(guān)鍵詞,單例模式的應(yīng)用也變得更加簡(jiǎn)便。
在實(shí)現(xiàn)單例中,我們有將單例和蒙層類功能拆分開,也有合在一起的,這取決于在你的項(xiàng)目中想要如何設(shè)計(jì),如果單例并非是大面積的組件套用,其實(shí)還是推薦合在一起,有助于后續(xù)在單文件中維護(hù)整個(gè)功能類。