日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
前端的設(shè)計模式系列-單例模式

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

創(chuàng)新互聯(lián)主營湛江網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,成都app開發(fā),湛江h(huán)5小程序開發(fā)搭建,湛江網(wǎng)站營銷推廣歡迎湛江等地區(qū)企業(yè)咨詢

下面按照「場景」-「設(shè)計模式定義」- 「代碼實現(xiàn)」- 「更多場景」-「總」的順序來總結(jié)一下,如有不當(dāng)之處,歡迎交流討論。

場景

如果需要實現(xiàn)一個全局的 loading 遮罩層,正常展示是這樣的:

但如果用戶連續(xù)調(diào)用 loaing 兩次,第二個遮罩層就會覆蓋掉第一個:

看起來就像出了 bug 一樣,因此我們需要采用單例模式,限制用戶同一時刻只能調(diào)用一個全局 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.”

可以說是最簡單的設(shè)計模式了,就是保證類的實例只有一個即可。

看一下 java 的示例:

public class Singleton {

private static final Singleton INSTANCE = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return INSTANCE;
}
}

上邊在初始化類的時候就進(jìn)行了創(chuàng)建對象,并且將構(gòu)造函數(shù)設(shè)置為 private 不允許外界調(diào)用,提供 getInstance 方法獲取對象。

還有一種 Lazy initialization 的模式,也就是延遲到調(diào)用 getInstance的時候才去創(chuàng)建對象。但如果多個線程中同時調(diào)用 getInstance 可能會導(dǎo)致創(chuàng)建多個對象,所以還需要進(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;
}
}

但單例模式存在很多爭議,比如可測試性不強(qiáng)、對抽象、繼承、多態(tài)都支持得不友好等等,但我感覺主要是基于 class 這類語言引起的問題,這里就不討論了。

回到 js ,模擬上邊實現(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 的實現(xiàn),事實上,js 創(chuàng)建對象并不一定需要通過 new 的方式,下邊我們詳細(xì)討論下。

js 的單例模式

首先單例模式產(chǎn)生的對象一般都是工具對象等,比如 jQuery 。它不需要我們通過構(gòu)造函數(shù)去傳參數(shù),所以就不需要去new 一個構(gòu)造函數(shù)去生成對象。

我們只需要通過字面量對象, var a = {} ,a 就可以看成一個單例對象了。

通常的單例對象可能會是下邊的樣子,暴露幾個方法供外界使用。

var Singleton = {
method1: function () {
// ...
},
method2: function () {
// ...
}
};

但如果Singleton 有私有屬性,可以寫成下邊的樣子:

var Singleton = {
privateVar: '我是私有屬性',
method1: function () {
// ...
},
method2: function () {
// ...
}
};

但此時外界就可以通過 Singleton 隨意修改 privateVar 的值。

為了解決這個問題,我們可以借助閉包,通過 IIFE (Immediately Invoked Function Expression) 將一些屬性和方法私有化。

var myInstance = (function() {
var privateVar = '';

function privateMethod () {
// ...
}

return {
method1: function () {
},
method2: function () {
}
};
})();

但隨著 ES6 、Webpack 的出現(xiàn),我們很少像上邊那樣去定義一個模塊了,而是通過單文件,一個文件就是一個模塊,同時也可以看成一個單例對象。

// singleton.js
const somePrivateState = []

function privateMethod () {
// ...
}

export default {
method1() {
// ...
},
method2() {
// ...
}
}

然后使用的時候 import 即可。

// main.js
import Singleton from './singleton.js'
// ...

即使有另一個文件也 import 了同一個文件。

// main2.js
import Singleton from './singleton.js'

但這兩個不同文件的 Singleton 仍舊是同一個對象,這是 ES Moudule 的特性。

那如果通過 Webpack 將 ES6 轉(zhuǎn)成ES5 以后呢,這種方式還會是單例對象嗎?

答案當(dāng)然是肯定的,可以看一下Webpack 打包的產(chǎn)物,其實就是使用了IIFE ,同時將第一次 import 的模塊進(jìn)行了緩存,第二次 import 的時候會使用之前的緩存。可以看下 __webpack_require__ 的實現(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;
}

代碼實現(xiàn)

回頭開頭我們說的全局 loading 的問題,解決起來也很簡單,同樣的,如果已經(jīng)有了 loading 的實例,我們只需要直接返回即可。

這里直接看一下 ElementUI 對于全局 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的時候,如果同時調(diào)用兩次,其實只會有一個 loading 的遮罩層,第二個并不會顯示。

mounted() {
const first = this.$loading({
text: '我是第一個全屏loading',
})

const second = this.$loading({
text: '我是第二個'
})

console.log(first === second); // true
}

更多場景

如果使用了 ES6 的模塊,其實就不用考慮單不單例的問題了,但如果我們使用的第三方庫,它沒有 export 一個實例對象,而是 export 一個 function/class 呢?

比如之前介紹的 發(fā)布-訂閱模式 的 Event 對象,這個肯定需要是全局單例的,如果我們使用 eventemitter3 這個 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 這個函數(shù)導(dǎo)出了,如果每個頁面都各自 import 它,然后通過 new EventEmitter() 來生成對象,那發(fā)布訂閱就亂套了,因為它們不是同一個對象了。

此時,我們可以新建一個模塊,然后 export 一個實例化對象,其他頁面去使用這個對象就實現(xiàn)單例模式了。

import EventEmitter from 'eventemitter3';
// 全局唯一的事件總線
const event = new EventEmitter();
export default event;

單例模式比較簡單,主要是保證全局對象唯一,但相對于通過 class 生成對象的單例模式,js 就很特殊了。

因為在 js 中我們可以直接生成對象,并且這個對象就是全局唯一,所以在 js中,單例模式是渾然天成的,我們平常并不會感知到。

尤其是現(xiàn)在開發(fā)使用 ES6 模塊,每個模塊也同樣是一個單例對象,平常業(yè)務(wù)開發(fā)中也很少去應(yīng)用單例模式,為了舉出上邊的例子真的是腦細(xì)胞耗盡了,哈哈。


文章題目:前端的設(shè)計模式系列-單例模式
當(dāng)前路徑:http://www.dlmjj.cn/article/dpsihpo.html