新聞中心
大部分講設(shè)計(jì)模式的文章都是使用的 Java、C++ 這樣的以類(lèi)為基礎(chǔ)的靜態(tài)類(lèi)型語(yǔ)言,作為前端開(kāi)發(fā)者,js 這門(mén)基于原型的動(dòng)態(tài)語(yǔ)言,函數(shù)成為了一等公民,在實(shí)現(xiàn)一些設(shè)計(jì)模式上稍顯不同,甚至簡(jiǎn)單到不像使用了設(shè)計(jì)模式,有時(shí)候也會(huì)產(chǎn)生些困惑。

創(chuàng)新互聯(lián)主營(yíng)湛江網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,成都app開(kāi)發(fā),湛江h(huán)5小程序開(kāi)發(fā)搭建,湛江網(wǎng)站營(yíng)銷(xiāo)推廣歡迎湛江等地區(qū)企業(yè)咨詢
下面按照「場(chǎng)景」-「設(shè)計(jì)模式定義」- 「代碼實(shí)現(xiàn)」- 「更多場(chǎng)景」-「總」的順序來(lái)總結(jié)一下,如有不當(dāng)之處,歡迎交流討論。
場(chǎng)景
如果需要實(shí)現(xiàn)一個(gè)全局的 loading 遮罩層,正常展示是這樣的:
但如果用戶連續(xù)調(diào)用 loaing 兩次,第二個(gè)遮罩層就會(huì)覆蓋掉第一個(gè):
看起來(lái)就像出了 bug 一樣,因此我們需要采用單例模式,限制用戶同一時(shí)刻只能調(diào)用一個(gè)全局 loading 。
單例模式
看下 維基百科 給的定義:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance. This is useful when exactly one object is needed to coordinate actions across the system.”
可以說(shuō)是最簡(jiǎn)單的設(shè)計(jì)模式了,就是保證類(lèi)的實(shí)例只有一個(gè)即可。
看一下 java 的示例:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
上邊在初始化類(lèi)的時(shí)候就進(jìn)行了創(chuàng)建對(duì)象,并且將構(gòu)造函數(shù)設(shè)置為 private 不允許外界調(diào)用,提供 getInstance 方法獲取對(duì)象。
還有一種 Lazy initialization 的模式,也就是延遲到調(diào)用 getInstance的時(shí)候才去創(chuàng)建對(duì)象。但如果多個(gè)線程中同時(shí)調(diào)用 getInstance 可能會(huì)導(dǎo)致創(chuàng)建多個(gè)對(duì)象,所以還需要進(jìn)行加鎖。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
但單例模式存在很多爭(zhēng)議,比如可測(cè)試性不強(qiáng)、對(duì)抽象、繼承、多態(tài)都支持得不友好等等,但我感覺(jué)主要是基于 class 這類(lèi)語(yǔ)言引起的問(wèn)題,這里就不討論了。
回到 js ,模擬上邊實(shí)現(xiàn)一下:
const Singleton = function () {
this.instance = null;
};
Singleton.getInstance = function (name) {
if (!this.instance) {
this.instance = new Singleton();
}
return this.instance;
};
const a = Singleton.getInstance();
const b = Singleton.getInstance();
console.log(a === b); // true
但上邊就真的是邯鄲學(xué)步一樣的模仿了 java 的實(shí)現(xiàn),事實(shí)上,js 創(chuàng)建對(duì)象并不一定需要通過(guò) new 的方式,下邊我們?cè)敿?xì)討論下。
js 的單例模式
首先單例模式產(chǎn)生的對(duì)象一般都是工具對(duì)象等,比如 jQuery 。它不需要我們通過(guò)構(gòu)造函數(shù)去傳參數(shù),所以就不需要去new 一個(gè)構(gòu)造函數(shù)去生成對(duì)象。
我們只需要通過(guò)字面量對(duì)象, var a = {} ,a 就可以看成一個(gè)單例對(duì)象了。
通常的單例對(duì)象可能會(huì)是下邊的樣子,暴露幾個(gè)方法供外界使用。
var Singleton = {
method1: function () {
// ...
},
method2: function () {
// ...
}
};
但如果Singleton 有私有屬性,可以寫(xiě)成下邊的樣子:
var Singleton = {
privateVar: '我是私有屬性',
method1: function () {
// ...
},
method2: function () {
// ...
}
};
但此時(shí)外界就可以通過(guò) Singleton 隨意修改 privateVar 的值。
為了解決這個(gè)問(wèn)題,我們可以借助閉包,通過(guò) IIFE (Immediately Invoked Function Expression) 將一些屬性和方法私有化。
var myInstance = (function() {
var privateVar = '';
function privateMethod () {
// ...
}
return {
method1: function () {
},
method2: function () {
}
};
})();
但隨著 ES6 、Webpack 的出現(xiàn),我們很少像上邊那樣去定義一個(gè)模塊了,而是通過(guò)單文件,一個(gè)文件就是一個(gè)模塊,同時(shí)也可以看成一個(gè)單例對(duì)象。
// singleton.js
const somePrivateState = []
function privateMethod () {
// ...
}
export default {
method1() {
// ...
},
method2() {
// ...
}
}
然后使用的時(shí)候 import 即可。
// main.js
import Singleton from './singleton.js'
// ...
即使有另一個(gè)文件也 import 了同一個(gè)文件。
// main2.js
import Singleton from './singleton.js'
但這兩個(gè)不同文件的 Singleton 仍舊是同一個(gè)對(duì)象,這是 ES Moudule 的特性。
那如果通過(guò) Webpack 將 ES6 轉(zhuǎn)成ES5 以后呢,這種方式還會(huì)是單例對(duì)象嗎?
答案當(dāng)然是肯定的,可以看一下Webpack 打包的產(chǎn)物,其實(shí)就是使用了IIFE ,同時(shí)將第一次 import 的模塊進(jìn)行了緩存,第二次 import 的時(shí)候會(huì)使用之前的緩存。可以看下 __webpack_require__ 的實(shí)現(xiàn),和單例模式的邏輯是一樣的。
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
// 單例模式的應(yīng)用
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
return module.exports;
}
代碼實(shí)現(xiàn)
回頭開(kāi)頭我們說(shuō)的全局 loading 的問(wèn)題,解決起來(lái)也很簡(jiǎn)單,同樣的,如果已經(jīng)有了 loading 的實(shí)例,我們只需要直接返回即可。
這里直接看一下 ElementUI 對(duì)于全局 loading 的處理。
// ~/packages/loading/src/index.js
let fullscreenLoading;
const Loading = (options = {}) => {
...
// options 不傳的話默認(rèn)是 fullscreen
options = merge({}, defaults, options);
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading; // 存在直接 return
}
let parent = options.body ? document.body : options.target;
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
});
...
if (options.fullscreen) {
fullscreenLoading = instance;
}
return instance;
};
這樣在使用 Element 的 loading的時(shí)候,如果同時(shí)調(diào)用兩次,其實(shí)只會(huì)有一個(gè) loading 的遮罩層,第二個(gè)并不會(huì)顯示。
mounted() {
const first = this.$loading({
text: '我是第一個(gè)全屏loading',
})
const second = this.$loading({
text: '我是第二個(gè)'
})
console.log(first === second); // true
}
更多場(chǎng)景
如果使用了 ES6 的模塊,其實(shí)就不用考慮單不單例的問(wèn)題了,但如果我們使用的第三方庫(kù),它沒(méi)有 export 一個(gè)實(shí)例對(duì)象,而是 export 一個(gè) function/class 呢?
比如之前介紹的 發(fā)布-訂閱模式 的 Event 對(duì)象,這個(gè)肯定需要是全局單例的,如果我們使用 eventemitter3 這個(gè) node 包,看一下它的導(dǎo)出:
'use strict';
var has = Object.prototype.hasOwnProperty
, prefix = '~';
/**
* Constructor to create a storage for our `EE` objects.
* An `Events` instance is a plain object whose properties are event names.
*
* @constructor
* @private
*/
function Events() {}
//
// We try to not inherit from `Object.prototype`. In some engines creating an
// instance in this way is faster than calling `Object.create(null)` directly.
// If `Object.create(null)` is not supported we prefix the event names with a
// character to make sure that the built-in object properties are not
// overridden or used as an attack vector.
//
if (Object.create) {
Events.prototype = Object.create(null);
//
// This hack is needed because the `__proto__` property is still inherited in
// some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5.
//
if (!new Events().__proto__) prefix = false;
}
/**
* Representation of a single event listener.
*
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} [once=false] Specify if the listener is a one-time listener.
* @constructor
* @private
*/
function EE(fn, context, once) {
this.fn = fn;
this.context = context;
this.once = once || false;
}
/**
* Add a listener for a given event.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} context The context to invoke the listener with.
* @param {Boolean} once Specify if the listener is a one-time listener.
* @returns {EventEmitter}
* @private
*/
function addListener(emitter, event, fn, context, once) {
if (typeof fn !== 'function') {
throw new TypeError('The listener must be a function');
}
var listener = new EE(fn, context || emitter, once)
, evt = prefix ? prefix + event : event;
if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++;
else if (!emitter._events[evt].fn) emitter._events[evt].push(listener);
else emitter._events[evt] = [emitter._events[evt], listener];
return emitter;
}
/**
* Clear event by name.
*
* @param {EventEmitter} emitter Reference to the `EventEmitter` instance.
* @param {(String|Symbol)} evt The Event name.
* @private
*/
function clearEvent(emitter, evt) {
if (--emitter._eventsCount === 0) emitter._events = new Events();
else delete emitter._events[evt];
}
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*
* @constructor
* @public
*/
function EventEmitter() {
this._events = new Events();
this._eventsCount = 0;
}
...
/**
* Add a listener for a given event.
*
* @param {(String|Symbol)} event The event name.
* @param {Function} fn The listener function.
* @param {*} [context=this] The context to invoke the listener with.
* @returns {EventEmitter} `this`.
* @public
*/
EventEmitter.prototype.on = function on(event, fn, context) {
return addListener(this, event, fn, context, false);
};
...
...
// Alias methods names because people roll like that.
//
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.addListener = EventEmitter.prototype.on;
//
// Expose the prefix.
//
EventEmitter.prefixed = prefix;
//
// Allow `EventEmitter` to be imported as module namespace.
//
EventEmitter.EventEmitter = EventEmitter;
//
// Expose the module.
//
if ('undefined' !== typeof module) {
module.exports = EventEmitter;
}
可以看到它直接將 EventEmitter 這個(gè)函數(shù)導(dǎo)出了,如果每個(gè)頁(yè)面都各自 import 它,然后通過(guò) new EventEmitter() 來(lái)生成對(duì)象,那發(fā)布訂閱就亂套了,因?yàn)樗鼈儾皇峭粋€(gè)對(duì)象了。
此時(shí),我們可以新建一個(gè)模塊,然后 export 一個(gè)實(shí)例化對(duì)象,其他頁(yè)面去使用這個(gè)對(duì)象就實(shí)現(xiàn)單例模式了。
import EventEmitter from 'eventemitter3';
// 全局唯一的事件總線
const event = new EventEmitter();
export default event;
總
單例模式比較簡(jiǎn)單,主要是保證全局對(duì)象唯一,但相對(duì)于通過(guò) class 生成對(duì)象的單例模式,js 就很特殊了。
因?yàn)樵?js 中我們可以直接生成對(duì)象,并且這個(gè)對(duì)象就是全局唯一,所以在 js中,單例模式是渾然天成的,我們平常并不會(huì)感知到。
尤其是現(xiàn)在開(kāi)發(fā)使用 ES6 模塊,每個(gè)模塊也同樣是一個(gè)單例對(duì)象,平常業(yè)務(wù)開(kāi)發(fā)中也很少去應(yīng)用單例模式,為了舉出上邊的例子真的是腦細(xì)胞耗盡了,哈哈。
分享標(biāo)題:前端的設(shè)計(jì)模式系列-單例模式
URL地址:http://fisionsoft.com.cn/article/dpsihpo.html


咨詢
建站咨詢
