享元模式不同于一般的設(shè)計(jì)模式,它主要用來(lái)優(yōu)化程序的性能,它最適合解決大量類(lèi)似的對(duì)象而產(chǎn)生的性能問(wèn)題。享元模式通過(guò)分析應(yīng)用程序的對(duì)象,將其解析為內(nèi)在數(shù)據(jù)和外在數(shù)據(jù),減少對(duì)象的數(shù)量,從而提高應(yīng)用程序的性能。
基本知識(shí)
享元模式通過(guò)共享大量的細(xì)粒度的對(duì)象,減少對(duì)象的數(shù)量,從而減少對(duì)象的內(nèi)存,提高應(yīng)用程序的性能。其基本思想就是分解現(xiàn)有類(lèi)似對(duì)象的組成,將其展開(kāi)為可以共享的內(nèi)在數(shù)據(jù)和不可共享的外在數(shù)據(jù),我們稱(chēng)內(nèi)在數(shù)據(jù)的對(duì)象為享元對(duì)象。通常還需要一個(gè)工廠類(lèi)來(lái)維護(hù)內(nèi)在數(shù)據(jù)。
在JS中,享元模式主要有下面幾個(gè)角色組成:
(1)客戶(hù)端:用來(lái)調(diào)用享元工廠來(lái)獲取內(nèi)在數(shù)據(jù)的類(lèi),通常是應(yīng)用程序所需的對(duì)象,
(2)享元工廠:用來(lái)維護(hù)享元數(shù)據(jù)的類(lèi)
(3)享元類(lèi):保持內(nèi)在數(shù)據(jù)的類(lèi)
享元模式的實(shí)現(xiàn)和應(yīng)用
一般實(shí)現(xiàn)
我們舉個(gè)例子進(jìn)行說(shuō)明:蘋(píng)果公司批量生產(chǎn)iphone,iphone的大部分?jǐn)?shù)據(jù)比如型號(hào),屏幕都是一樣,少數(shù)部分?jǐn)?shù)據(jù)比如內(nèi)存有分16G,32G等。未使用享元模式前,我們寫(xiě)代碼如下:
function Iphone(model, screen, memory, SN) {
this. model = model;
this.screen = screen;
this.memory = memory;
this.SN = SN;
}
var phones = [];
for (var i = 0; i < 1000000; i++) {
var memory = i % 2 == 0 ? 16 : 32;
phones.push(new Iphone("iphone6s", 5.0, memory, i));
}
這段代碼中,創(chuàng)建了一百萬(wàn)個(gè)iphone,每個(gè)iphone都獨(dú)立申請(qǐng)一個(gè)內(nèi)存。但是我們仔細(xì)觀察可以看到,大部分iphone都是類(lèi)似的,只是內(nèi)存和序列號(hào)不一樣,如果是一個(gè)對(duì)性能要求比較高的程序,我們就要考慮去優(yōu)化它。
大量相似對(duì)象的程序,我們就可以考慮用享元模式去優(yōu)化它,我們分析出大部分的iphone的型號(hào),屏幕,內(nèi)存都是一樣的,那這部分?jǐn)?shù)據(jù)就可以公用,就是享元模式中的內(nèi)在數(shù)據(jù),定義享元類(lèi)如下:
function IphoneFlyweight(model, screen, memory) {
this.model = model;
this.screen = screen;
this.memory = memory;
}
我們定義了iphone的享元類(lèi),其中包含型號(hào),屏幕和內(nèi)存三個(gè)數(shù)據(jù)。我們還需要一個(gè)享元工廠來(lái)維護(hù)這些數(shù)據(jù):
var flyweightFactory = (function () {
var iphones = {};
return {
get: function (model, screen, memory) {
var key = model + screen + memory;
if (!iphones[key]) {
iphones[key] = new IphoneFlyweight(model, screen, memory);
}
return iphones[key];
}
};
})();
在這個(gè)工廠中,我們定義了一個(gè)字典來(lái)保存享元對(duì)象,提供一個(gè)方法根據(jù)參數(shù)來(lái)獲取享元對(duì)象,如果字典中有則直接返回,沒(méi)有則創(chuàng)建一個(gè)返回。
接著我們創(chuàng)建一個(gè)客戶(hù)端類(lèi),這個(gè)客戶(hù)端類(lèi)就是修改自iphone類(lèi):
function Iphone(model, screen, memory, SN) {
this.flyweight = flyweightFactory.get(model, screen, memory);
this.SN = SN;
}
然后我們依舊像之間那樣生成多個(gè)iphone
var phones = [];
for (var i = 0; i < 1000000; i++) {
var memory = i % 2 == 0 ? 16 : 32;
phones.push(new Iphone("iphone6s", 5.0, memory, i));
}
console.log(phones);
這里的關(guān)鍵就在于Iphone構(gòu)造函數(shù)里面的this.flyweight = flyweightFactory.get(model, screen, memory)。這句代碼通過(guò)享元工廠去獲取享元數(shù)據(jù),而在享元工廠里面,如果已經(jīng)存在相同數(shù)據(jù)的對(duì)象則會(huì)直接返回對(duì)象,多個(gè)iphone對(duì)象共享這部分相同的數(shù)據(jù),所以原本類(lèi)似的數(shù)據(jù)已經(jīng)大大減少,減少的內(nèi)存的占用。
享元模式在DOM中的應(yīng)用
享元模式的一個(gè)典型應(yīng)用就是DOM事件操作,DOM事件機(jī)制分成事件冒泡和事件捕獲。我們簡(jiǎn)單介紹一下這兩者:
事件冒泡:綁定的事件從最里層的元素開(kāi)始觸發(fā),然后冒泡到最外層
事件捕獲:綁定的事件從最外層的元素開(kāi)始觸發(fā),然后傳到最里層
假設(shè)我們HTML中有一個(gè)菜單列表
<ul class="menu">
<li class="item">選項(xiàng)1</li>
<li class="item">選項(xiàng)2</li>
<li class="item">選項(xiàng)3</li>
<li class="item">選項(xiàng)4</li>
<li class="item">選項(xiàng)5</li>
<li class="item">選項(xiàng)6</li>
</ul>
點(diǎn)擊菜單項(xiàng),進(jìn)行相應(yīng)的操作,我們通過(guò)jQuery來(lái)綁定事件,一般會(huì)這么做:
$(".item").on("click", function () {
console.log($(this).text());
})
給每個(gè)列表項(xiàng)綁定事件,點(diǎn)擊輸出相應(yīng)的文本。這樣看暫時(shí)沒(méi)有什么問(wèn)題,但是如果是一個(gè)很長(zhǎng)的列表,尤其是在移動(dòng)端特別長(zhǎng)的列表時(shí),就會(huì)有性能問(wèn)題,因?yàn)槊總€(gè)項(xiàng)都綁定了事件,都占用了內(nèi)存。但是這些事件處理程序其實(shí)都是很類(lèi)似的,我們就要對(duì)其優(yōu)化。
$(".menu").on("click", ".item", function () {
console.log($(this).text());
})
通過(guò)這種方式進(jìn)行事件綁定,可以減少事件處理程序的數(shù)量,這種方式叫做事件委托,也是運(yùn)用了享元模式的原理。事件處理程序是公用的內(nèi)在部分,每個(gè)菜單項(xiàng)各自的文本就是外在部分。我們簡(jiǎn)單說(shuō)下事件委托的原理:點(diǎn)擊菜單項(xiàng),事件會(huì)從li元素冒泡到ul元素,我們綁定事件到ul上,實(shí)際上就綁定了一個(gè)事件,然后通過(guò)事件參數(shù)event里面的target來(lái)判斷點(diǎn)擊的具體是哪一個(gè)元素,比如低級(jí)第一個(gè)li元素,event.target就是li,這樣就能拿到具體的點(diǎn)擊元素了,就可以根據(jù)不同元素進(jìn)行不同的處理。
總結(jié)
享元模式是一種優(yōu)化程序性能的手段,通過(guò)共享公用數(shù)據(jù)來(lái)減少對(duì)象數(shù)量以達(dá)到優(yōu)化程序的手段。享元模式適用于擁有大量類(lèi)似對(duì)象并且對(duì)性能有要求的場(chǎng)景。因?yàn)橄碓J叫枰蛛x內(nèi)部和外部數(shù)據(jù),增加了程序的邏輯復(fù)雜性,建議對(duì)性能有要求的時(shí)候才使用享元模式。
享元模式之利:
可以把網(wǎng)頁(yè)的資源符合降低幾個(gè)數(shù)量級(jí)。即使享元模式的應(yīng)用無(wú)法將實(shí)例的個(gè)數(shù)削減到一個(gè),你仍能夠從中獲益不少。
這種節(jié)省不需要大量修改原有代碼。在創(chuàng)建了管理器、工廠和享元之后,就需要對(duì)代碼進(jìn)行的修改只不過(guò)是從直接實(shí)例化目標(biāo)類(lèi)改為調(diào)用管理器對(duì)象的某個(gè)方法。
享元模式之弊:
如果把它用在不必要的地方,其結(jié)果反而有損代碼的運(yùn)行效率。這種模式在優(yōu)化代碼的同時(shí),也提高了其復(fù)雜程度,這會(huì)給調(diào)試和維護(hù)造成困難。
它之所以會(huì)妨礙調(diào)試,是因?yàn)楝F(xiàn)在可能出錯(cuò)的地方變成了三個(gè):管理器、工廠和享元。
這種優(yōu)化也會(huì)使維護(hù)變得更加困難。現(xiàn)在你面對(duì)的不是由封裝著數(shù)據(jù)的對(duì)象構(gòu)成的清晰架構(gòu),而是一堆又碎又亂的東西。其中的數(shù)據(jù)至少分兩處保存。最好注釋標(biāo)明內(nèi)在數(shù)據(jù)和外在數(shù)據(jù)。
只有在必要的時(shí)候才應(yīng)該進(jìn)行這種優(yōu)化。必須在運(yùn)行效率和可維護(hù)性之間進(jìn)行權(quán)衡。如果拿不準(zhǔn)是否需要使用享元模式,那么你很可能并不需要它。享元模式適合的是系統(tǒng)資源已經(jīng)用得差不多而且明顯需要進(jìn)行某種優(yōu)化這樣一類(lèi)場(chǎng)合。
這種模式對(duì)Javascript程序員特別有用,因?yàn)樗梢杂脕?lái)減少網(wǎng)頁(yè)上所要使用的DOM元素的數(shù)量,要知道這些元素需要耗費(fèi)許多內(nèi)存。結(jié)合使用這種模式與組合模式等組織型可以開(kāi)發(fā)出功能豐富的復(fù)雜Web應(yīng)用系統(tǒng),它們可以平穩(wěn)的運(yùn)行在任何現(xiàn)代Javascript環(huán)境中。
享元模式的適用場(chǎng)合:
網(wǎng)頁(yè)中必須使用了大量資源密集型對(duì)象。如果只會(huì)用到少許這類(lèi)對(duì)象,這種優(yōu)化并不劃算。
對(duì)象中所保存的數(shù)據(jù)至少有一部分能被轉(zhuǎn)化為外在數(shù)據(jù)。此外,將這些數(shù)據(jù)存儲(chǔ)在對(duì)象外部所占用的資源應(yīng)該相對(duì)較少,否則這種做法對(duì)于性能的提示實(shí)際上毫無(wú)意義。那種大量包含基礎(chǔ)性代碼和HTML內(nèi)容的對(duì)象可能比較適合這種優(yōu)化。
將外在數(shù)據(jù)分離出去后,獨(dú)一無(wú)二的對(duì)象的數(shù)目相對(duì)較少。