享元模式是一個優(yōu)化重復(fù)、緩慢和低效數(shù)據(jù)共享代碼的經(jīng)典結(jié)構(gòu)化解決方案。它的目標是以相關(guān)對象盡可能多的共享數(shù)據(jù),來減少應(yīng)用程序中內(nèi)存的使用(例如:應(yīng)用程序的配置、狀態(tài)等)。
此模式最先由Paul Calder 和 Mark Linton在1990提出,并用拳擊等級中少于112磅體重的等級名稱來命名。享元(“Flyweight”英語中的輕量級)的名稱本身是從以幫以助我們完成減少重量(內(nèi)存標記)為目標的重量等級推導(dǎo)出的。
實際應(yīng)用中,輕量級的數(shù)據(jù)共享采集被多個對象使用的相似對象或數(shù)據(jù)結(jié)構(gòu),并將這些數(shù)據(jù)放置于單個的擴展對象中。我們可以把它傳遞給依靠這些數(shù)據(jù)的對象,而不是在他們每個上面都存儲一次。
有兩種方法來使用享元。第一種是數(shù)據(jù)層,基于存儲在內(nèi)存中的大量相同對象的數(shù)據(jù)共享的概念。第二種是DOM層,享元模式被作為事件管理中心,以避免將事件處理程序關(guān)聯(lián)到我們需要相同行為父容器的所有子節(jié)點上。 享元模式通常被更多的用于數(shù)據(jù)層,我們先來看看它。
對于這個應(yīng)用程序而言,圍繞經(jīng)典的享元模式有更多需要我們意識到的概念。享元模式中有一個兩種狀態(tài)的概念——內(nèi)在和外在。內(nèi)在信息可能會被我們的對象中的內(nèi)部方法所需要,它們絕對不可以作為功能被帶出。外在信息則可以被移除或者放在外部存儲。
帶有相同內(nèi)在數(shù)據(jù)的對象可以被一個單獨的共享對象所代替,它通過一個工廠方法被創(chuàng)建出來。這允許我們?nèi)ワ@著降低隱式數(shù)據(jù)的存儲數(shù)量。
個中的好處是我們能夠留心于已經(jīng)被初始化的對象,讓只有不同于我們已經(jīng)擁有的對象的內(nèi)在狀態(tài)時,新的拷貝才會被創(chuàng)建。
我們使用一個管理器來處理外在狀態(tài)。如何實現(xiàn)可以有所不同,但針對此的一種方法就是讓管理器對象包含一個存儲外在狀態(tài)以及它們所屬的享元對象的中心數(shù)據(jù)庫。
近幾年享元模式已經(jīng)在Javascript中得到了深入的應(yīng)用,我們會用到的許多實現(xiàn)方式其靈感來自于Java和C++的世界。
我們第一個要來看的關(guān)于享元模式的代碼是我的對來自維基百科的針對享元模式的 Java 示例的 Javascript 實現(xiàn)。
在這個實現(xiàn)中我們將要使用如下所列的三種類型的享元組件:
這些對應(yīng)于我們實現(xiàn)中的如下定義:
鴨式?jīng)_減允許我們擴展一種語言或者解決方法的能力,而不需要變更運行時的源。由于接下的方案需要使用一個Java關(guān)鍵字“implements”來實現(xiàn)接口,而在Javascript本地看不到這種方案,那就讓我們首先來對它進行鴨式?jīng)_減。
Function.prototype.implementsFor 在一個對象構(gòu)造器上面起作用,并且將接受一個父類(函數(shù)—)或者對象,而從繼承于普通的繼承(對于函數(shù)而言)或者虛擬繼承(對于對象而言)都可以。
// Simulate pure virtual inheritance/"implement" keyword for JS Function.prototype.implementsFor = function( parentClassOrObject ){ if ( parentClassOrObject.constructor === Function ) { // Normal Inheritance this.prototype = new parentClassOrObject(); this.prototype.constructor = this; this.prototype.parent = parentClassOrObject.prototype; } else { // Pure Virtual Inheritance this.prototype = parentClassOrObject; this.prototype.constructor = this; this.prototype.parent = parentClassOrObject; } return this; };
我們可以通過讓一個函數(shù)明確的繼承自一個接口來彌補implements關(guān)鍵字的缺失。下面,為了使我們得以去分配支持一個對象的這些實現(xiàn)的功能,CoffeeFlavor實現(xiàn)了CoffeeOrder接口,并且必須包含其接口的方法。
// Flyweight object
var CoffeeOrder = {
// Interfaces
serveCoffee:function(context){},
getFlavor:function(){}
};
// ConcreteFlyweight object that creates ConcreteFlyweight
// Implements CoffeeOrder
function CoffeeFlavor( newFlavor ){
var flavor = newFlavor;
// If an interface has been defined for a feature
// implement the feature
if( typeof this.getFlavor === "function" ){
this.getFlavor = function() {
return flavor;
};
}
if( typeof this.serveCoffee === "function" ){
this.serveCoffee = function( context ) {
console.log("Serving Coffee flavor "
+ flavor
+ " to table number "
+ context.getTable());
};
}
}
// Implement interface for CoffeeOrder
CoffeeFlavor.implementsFor( CoffeeOrder );
// Handle table numbers for a coffee order
function CoffeeOrderContext( tableNumber ) {
return{
getTable: function() {
return tableNumber;
}
};
}
function CoffeeFlavorFactory() {
var flavors = {},
length = 0;
return {
getCoffeeFlavor: function (flavorName) {
var flavor = flavors[flavorName];
if (flavor === undefined) {
flavor = new CoffeeFlavor(flavorName);
flavors[flavorName] = flavor;
length++;
}
return flavor;
},
getTotalCoffeeFlavorsMade: function () {
return length;
}
};
}
// Sample usage:
// testFlyweight()
function testFlyweight(){
// The flavors ordered.
var flavors = new CoffeeFlavor(),
// The tables for the orders.
tables = new CoffeeOrderContext(),
// Number of orders made
ordersMade = 0,
// The CoffeeFlavorFactory instance
flavorFactory;
function takeOrders( flavorIn, table) {
flavors[ordersMade] = flavorFactory.getCoffeeFlavor( flavorIn );
tables[ordersMade++] = new CoffeeOrderContext( table );
}
flavorFactory = new CoffeeFlavorFactory();
takeOrders("Cappuccino", 2);
takeOrders("Cappuccino", 2);
takeOrders("Frappe", 1);
takeOrders("Frappe", 1);
takeOrders("Xpresso", 1);
takeOrders("Frappe", 897);
takeOrders("Cappuccino", 97);
takeOrders("Cappuccino", 97);
takeOrders("Frappe", 3);
takeOrders("Xpresso", 3);
takeOrders("Cappuccino", 3);
takeOrders("Xpresso", 96);
takeOrders("Frappe", 552);
takeOrders("Cappuccino", 121);
takeOrders("Xpresso", 121);
for (var i = 0; i < ordersMade; ++i) {
flavors[i].serveCoffee(tables[i]);
}
console.log(" ");
console.log("total CoffeeFlavor objects made: " + flavorFactory.getTotalCoffeeFlavorsMade());
} <span style="line-height:1.5;font-family:'sans serif', tahoma, verdana, helvetica;font-size:10pt;"></span>
接下來,讓我們通過實現(xiàn)一個管理一個圖書館中所有書籍的系統(tǒng)來繼續(xù)觀察享元。分析得知每一本書的重要元數(shù)據(jù)如下:
我們也將需要下面一些屬性,來跟蹤哪一個成員是被借出的一本特定的書,借出它們的日期,還有預(yù)計的歸還日期。
var Book = function( id, title, author, genre, pageCount,publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate,availability ){
this.id = id;
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = dueReturnDate;
this.availability = availability;
};
Book.prototype = {
getTitle: function () {
return this.title;
},
getAuthor: function () {
return this.author;
},
getISBN: function (){
return this.ISBN;
},
// For brevity, other getters are not shown
updateCheckoutStatus: function( bookID, newStatus, checkoutDate , checkoutMember, newReturnDate ){
this.id = bookID;
this.availability = newStatus;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = newReturnDate;
},
extendCheckoutPeriod: function( bookID, newReturnDate ){
this.id = bookID;
this.dueReturnDate = newReturnDate;
},
isPastDue: function(bookID){
var currentDate = new Date();
return currentDate.getTime() > Date.parse( this.dueReturnDate );
}
};
這對于最初小規(guī)模的藏書可能工作得還好,然而當圖書館擴充至每一本書的多個版本和可用的備份,這樣一個大型的庫存,我們會發(fā)現(xiàn)管理系統(tǒng)的運行隨著時間的推移會越來越慢。使用成千上萬的書籍對象可能會壓倒內(nèi)存,而我們可以通過享元模式的提升來優(yōu)化我們的系統(tǒng)。
現(xiàn)在我們可以像下面這樣將我們的數(shù)據(jù)分離成為內(nèi)在和外在的狀態(tài):同書籍對象(標題,版權(quán)歸屬)相關(guān)的數(shù)據(jù)是內(nèi)在的,而借出數(shù)據(jù)(借出成員,規(guī)定歸還日期)則被看做是外在的。這實際上意味著對于每一種書籍屬性的組合僅需要一個書籍對象。這仍然具有相當大的數(shù)量,但相比之前已經(jīng)得到大大的縮減了。
下面的書籍元數(shù)據(jù)組合的單一實體將在所有帶有一個特定標題的書籍拷貝中共享。
// Flyweight optimized version
var Book = function ( title, author, genre, pageCount, publisherID, ISBN ) {
this.title = title;
this.author = author;
this.genre = genre;
this.pageCount = pageCount;
this.publisherID = publisherID;
this.ISBN = ISBN;
};
如我們所見,外在狀態(tài)已經(jīng)被移除了。從圖書館借出所要做的一切都被轉(zhuǎn)移到一個管理器中,由于對象數(shù)據(jù)現(xiàn)在是分段的,工廠可以被用來做實例化。
現(xiàn)在讓我們定義一個非?;镜墓S。我們用它做的工作是,執(zhí)行一個檢查來看看一本給定標題的書是不是之前已經(jīng)在系統(tǒng)內(nèi)創(chuàng)建過了;如果創(chuàng)建過了,我們就返回它 - 如果沒有,一本新書就會被創(chuàng)建并保存,使得以后可以訪問它。這確保了為每一條本質(zhì)上唯一的數(shù)據(jù),我們只創(chuàng)建了一份單一的拷貝:
// Book Factory singleton
var BookFactory = (function () {
var existingBooks = {}, existingBook;
return {
createBook: function ( title, author, genre, pageCount, publisherID, ISBN ) {
// Find out if a particular book meta-data combination has been created before
// !! or (bang bang) forces a boolean to be returned
existingBook = existingBooks[ISBN];
if ( !!existingBook ) {
return existingBook;
} else {
// if not, let's create a new instance of the book and store it
var book = new Book( title, author, genre, pageCount, publisherID, ISBN );
existingBooks[ISBN] = book;
return book;
}
}
};
});
下一步,我們需要將那些從Book對象中移除的狀態(tài)存儲到某一個地方——幸運的是一個管理器(我們會將其定義成一個單例)可以被用來封裝它們。書籍對象和借出這些書籍的圖書館成員的組合將被稱作書籍借出記錄。這些我們的管理器都將會存儲,并且也包含我們在對Book類進行享元優(yōu)化期間剝離的同借出相關(guān)的邏輯。
// BookRecordManager singleton
var BookRecordManager = (function () {
var bookRecordDatabase = {};
return {
// add a new book into the library system
addBookRecord: function ( id, title, author, genre, pageCount, publisherID, ISBN, checkoutDate, checkoutMember, dueReturnDate, availability ) {
var book = bookFactory.createBook( title, author, genre, pageCount, publisherID, ISBN );
bookRecordDatabase[id] = {
checkoutMember: checkoutMember,
checkoutDate: checkoutDate,
dueReturnDate: dueReturnDate,
availability: availability,
book: book
};
},
updateCheckoutStatus: function ( bookID, newStatus, checkoutDate, checkoutMember, newReturnDate ) {
var record = bookRecordDatabase[bookID];
record.availability = newStatus;
record.checkoutDate = checkoutDate;
record.checkoutMember = checkoutMember;
record.dueReturnDate = newReturnDate;
},
extendCheckoutPeriod: function ( bookID, newReturnDate ) {
bookRecordDatabase[bookID].dueReturnDate = newReturnDate;
},
isPastDue: function ( bookID ) {
var currentDate = new Date();
return currentDate.getTime() > Date.parse( bookRecordDatabase[bookID].dueReturnDate );
}
};
});
這些改變的結(jié)果是所有從Book類中擷取的數(shù)據(jù)現(xiàn)在被存儲到了BookManager單例(BookDatabase)的一個屬性之中——與我們以前使用大量對象相比可以被認為是更加高效的東西。同書籍借出相關(guān)的方法也被設(shè)置在這里,因為它們處理的數(shù)據(jù)是外在的而不內(nèi)在的。
這個過程確實給我們最終的解決方法增加了一點點復(fù)雜性,然而同已經(jīng)明智解決的數(shù)據(jù)性能問題相比,這只是一個小擔(dān)憂,如果我們有同一本書的30份拷貝,現(xiàn)在我們只需要存儲它一次就夠了。每一個函數(shù)也會占用內(nèi)存。使用享元模式這些函數(shù)只在一個地方存在(就是在管理器上),并且不是在每一個對象上面,這節(jié)約了內(nèi)存上的使用。
DOM(文檔對象模型)支持兩種允許對象偵聽事件的方法——自頂向下(事件捕獲)或者自底向下(時間冒泡)。 在事件捕獲中,事件一開始會被最外面的元素捕獲,并且傳播到最里面的元素。在事件冒泡中,事件被捕獲并且被賦給了最里面的元素,然后傳播到最外面的元素。
在此背景下描述享元模式的最好隱喻來自Gary Chisholm寫的文章,這里摘錄了一點點:
嘗試用一種池塘的方式思考享元模式。一只魚張開了它的嘴巴(事件發(fā)生了),泡泡一直要上升到表面(冒泡),當泡泡到達表面時,停泊在頂部的一直蒼蠅飛走了(動作執(zhí)行)。在這個示例中我們能夠很容易的將魚張開嘴巴轉(zhuǎn)換為按鈕被點擊了一下,將泡泡轉(zhuǎn)換為冒泡效果,而蒼蠅飛走了表示一些需要運行的函數(shù)。
冒泡被引入用來處理單個事件(比如:一次點擊)可能會由在DOM層級中的不同級別的多個事件處理器處理,這樣的場景。這在哪里發(fā)生了,事件冒泡就會為在盡可能最低的級別定義的事件處理器執(zhí)行。從那里開始,事件向上冒泡,一直到包含比應(yīng)該包含的更高層級的元素。
享元模式可用來進一步調(diào)整事件冒泡過程,這我們很快就將會看到。
一起來看看我們第一例子,當用戶有個動作(如點擊或是鼠標移動)時我們將有很多相似的文檔對象以及相似的行為要處理。一般情況下,當我們構(gòu)建手風(fēng)琴式控件,菜單以及其它列表控件時,就會在每一個超鏈接元素父容器里綁定點擊事件(如,$('ul li a').on(..)(jQuery代碼,譯者注))。我們可以方便的在可以監(jiān)聽事件容器里添加Flyweight,而不是在很多元素里綁定點擊事件。這樣就可處理或是簡單或是復(fù)雜的需求。
提到組件的類型,經(jīng)常會涉及到很多部分都有同樣重復(fù)的標簽(如,手風(fēng)琴式控件),這是個好機會,每個元素都有可能被點擊的行為,而且基本上用相同的類。我們可以用Flyweight來構(gòu)建一個基本的手風(fēng)琴控件。
這里我們使用一個stateManager命名空間來封裝我們的享元邏輯,同時使用jQuery來把初始點擊事件綁定到一個div容器上。為了確保頁面上沒有其他程序邏輯把類似的處理器綁定到該容器上,首先使用了一個unbind事件。
現(xiàn)在明確的確立一下容器中的那個子元素會被點擊,我們使用一次對target的檢查來提供對被點擊元素的引用,而不管它的父元素是誰。然后我們利用該信息來處理點擊事件,而實際上不需要在頁面裝載時把該事件綁定到具體的子元素上。
<div id="container">
<div class="toggle" href="#">More Info (Address)
<span class="info">
This is more information
</span></div>
<div class="toggle" href="#">Even More Info (Map)
<span class="info">
<iframe src="https://atts.w3cschool.cn/attachments/image/cimg/extmap.php?name=London&address=london%2C%20england&width=500...gt;"</iframe>
</span>
</div>
</div>
var stateManager = {
fly: function () {
var self = this;
$( "#container" ).unbind().on( "click" , function ( e ) {
var target = $( e.originalTarget || e.srcElement );
if ( target.is( "div.toggle") ) {
self.handleClick( target );
}
});
},
handleClick: function ( elem ) {
elem.find( "span" ).toggle( "slow" );
}
};
這樣做的好處是,我們把許多不相關(guān)的動作轉(zhuǎn)換為一個可以共享的動作(也許會保存在內(nèi)存中)。
在我們的第二個示例中,我們將會引述通過使用jQuery的享元可以獲得的一些更多的性能上的收獲。
Jame Padolsey 以前寫過一篇叫做76比特的文章,講述更快的jQuery,在其中他提醒我們每一次jQuery觸發(fā)了一個回調(diào),不管是什么類型(過濾器,每一個,事件處理器),我們都能夠通過this關(guān)鍵字訪問函數(shù)的上下文(與它相關(guān)的DOM元素)。
不幸的是,我們中的許多人已經(jīng)習(xí)慣將this封裝到$()或者jQuery()中的想法,這意味著新的jQuery實體沒必要每次都被構(gòu)造出來,而是簡單的這樣做:
$("div").on( "click", function () {
console.log( "You clicked: " + $( this ).attr( "id" ));
});
// we should avoid using the DOM element to create a
// jQuery object (with the overhead that comes with it)
// and just use the DOM element itself like this:
$( "div" ).on( "click", function () {
console.log( "You clicked:" + this.id );
});
James想要下面的場景中使用jQuery的jQuery.text,然而他不能茍同一個新的jQuery對象必須在每次迭代中創(chuàng)建的概念。
$( "a" ).map( function () {
return $( this ).text();
});
現(xiàn)在就使用jQuery的工具方法進行多余的包裝而言,使用jQuery.methodName(如,jQuery.text)比jQuery.fn.methodName(如,jQuery.fn.text)更好,這里methodName代表了一種使用的工具,如each()或者text。這避免了調(diào)用更深遠級別的抽象,或者每一次當我們的函數(shù)被調(diào)用時就構(gòu)造一個新的jQuery對象,因為定義了jQuery.methodName的庫本身在更底層使用jQuery.fn.methodName驅(qū)動的。
然而由于并不是所有jQuery的方法都有相應(yīng)的單節(jié)點功能,Padolsey根據(jù)這個創(chuàng)意設(shè)計了jQuery.single工具。 這里的創(chuàng)意是一個單獨的jQuery對象會被被創(chuàng)建出來并且用于每一次對jQuery.single的調(diào)用(有意義的是僅有一個jQuery對象會被創(chuàng)建出來)。對于此的實現(xiàn)可以在下面看到,而且由于我們將來自多個可能的對象的數(shù)據(jù)整合到一個更加集中的單一結(jié)構(gòu)中,技術(shù)上講,它也是一個享元。
jQuery.single = (function( o ){
var collection = jQuery([1]);
return function( element ) {
// Give collection the element:
collection[0] = element;
// Return the collection:
return collection;
};
});
對于這個的帶有調(diào)用鏈的動作的示例如下:
$( "div" ).on( "click", function () {
var html = jQuery.single( this ).next().html();
console.log( html );
});
注意:盡管我們可能相信通過簡單的緩存我們的jQuery代碼會提供出同等良好的性能收獲,但Padolsey聲稱$.single()仍然值得使用,且表現(xiàn)更好。那并不是說不使用任何的緩存,只要對這種方法的助益做到心里有數(shù)就行。想要對$.single有更加詳細的了解,建議你卻讀一讀Padolsey完整的文章。
更多建議: