享元模式
享元模式是一個經典的代碼優化的解決方案,主要針對重復的,緩慢的,低效的共享數據。它的目的最小化應用中的內存占用,通過與相關對象盡可能多的共享數據。
在實際中,輕量級的數據共享可以涉及幾個相似的對象或者數據結構,被用在大量的對象,并且將這些數據放在一個外部的對象中。我們可以傳遞這個對象給那些依賴數據的地方,而不是在每隔需要這些數據的地方存儲相同的對象。
使用享元
這有兩種可以應用享元的方式。第一種是數據層,我們根據數據共享的概念處理內存中大量相似的對象。
第二種是在dom處理層。享元可以被用于一個事件管理總局,在父容器中添加我們希望擁有的相似行為,避免為每個子元素添加事件處理程序。
數據層是享元模式最常用到的地方,我們將首先看看它。
享元和數據共享
在這個應用中,這里有幾個關于經典享元模式的概念需要我們注意一下。享元模式中有一個概念即兩種狀態——內部的和外部的。內部的信息可能被我們對象的內部方法需要,他們絕對不能沒有。外部的信息可以刪除或存儲于外部。
擁有相同內部數據的對象可以被單個的共享對象替換,通過工廠方法創建。這允許我們減少被隱性存儲的數據的數量。
這樣做的好處是,我們能夠對已經被實例化的對象保持關注,這樣新的副本就只能被創建,因為內在的狀態與我們已經擁有的對象不同。
我們使用一個管理者來處理外在的狀態,它被如何實現可能會有變化,但是有一種方法是管理對象對外部的狀態包含一個數據庫并且享元對象屬于其中。
實現經典的享元模式
今幾年享元模式并未在js中被大量使用。很多的實現我們可以使用Java或者C++世界中得到的靈感完成。
在下面的實現中我們將利用三種類型的享元組件,如下:
a.享元相當于一個接口,在外部狀態中通過享元可以接受和處理。
b.具體的享元實際上實現了享元的接口并且存儲內部狀態。
c.享元工廠管理享元對象并且創建他們。它確保我們的享元被共享,且管理他們作為一個對象組以便在我們需要個別的實例時候可以查找到。如果一個對象已經在被創建在組中,則直接返回該對象,否則,添加一個新的對象到池中并且返回他。
在我們的實現中符合下面的定義
CoffeeOrder: Flyweight
CoffeeFlavor: Concrete Flyweight
CoffeeOrderContext: Helper
CoffeeFlavorFactory: Flyweight Factory
testFlyweight: Utilization of our Flyweights
Duck punching "implements"
Duck punching 允許我們擴展一個語言或者解決方案的能力,而沒必要去修改運行時的代碼。下面的解決方案中需要一個Java中的關鍵字去實現接口,但是在js中并沒有提供此關鍵字,首先來讓我們duck punch 它。
Function.prototype.implementsFor
在一個對象的構造函數中工作,它接受一個父類(函數)或者對象并且使用正常繼承或者虛擬繼承繼承這它。
// 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;
};
我們可以利用這個去修補缺少“implements”關鍵字通過一個函數顯示的繼承一個接口。在下面,CoffeeFlavor實現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 (typeof 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());
}
用享元模式修改代碼
接下來讓我們來看一個圖書館的案例。每一本書的元數據可被分解為如下:
ID
Title
Author
Genre
Page count
Publisher ID
ISBN
我們還需要一個屬性來記錄成員已經借出了一本書,以及何時借的何時還的。
checkoutDate
checkoutMember
dueReturnDate
availability
每本書都可以按如下的方法表示,使用享元模式優化之前如下:
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 );
}
};
當書籍量很小的時候這種方式可能是有效的,然而,雖然著圖書館的擴張,每本書將包含很多版本和副本。隨著時間的流逝我們將發現系統運行的越來越慢。使用成千上萬的書籍對象可能導致內存不夠用,但我們可以使用享元模式來優化和改進我們的系統。
現在我們可以把我們的數據分為內部的和外部的規定如下:本書的對象相關的數據(標題、作者等)是內部的而檢驗數據(checkoutmember,duereturndate等)被認為是外部的。實際上著意味著,一個書籍對象需要書籍每個屬性的集合。他仍然是需要很多對象,但是比之前來說已經少多了。
接下來單個的圖書元數據實例組合將被分享在所有的書籍副本中通過特定的標題。
// 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;
};
正如我們所看到的,外部狀態已被刪除。與庫檢查有關的一切都將被移動到一個管理器中,且對象現在已經被分割,可以通過工廠模式實例化他們。
一個基本的工廠
現在讓我們定義一個非常基礎的工廠。我們要做的是檢查一個含有特定標題的書在我們的系統之中是否已經被創建。如果已經被創建,則直接返回他,否則,一個新的書籍將被創建并且存儲以便之后可以訪問他。這確保我們對于每個獨特的內在數據塊只創建一個單獨的副本。
// 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;
}
}
};
})();
管理外部的狀態
接下來,我們需要存儲那些從書對象中移除的狀態--幸運的是一個管理器(我們將定義為一個單獨的)可以用來封裝它們。一個書對象和庫成員的組合,將它們檢查出來將被稱為圖書記錄。我們的管理者將存儲并且包括檢測相關的我們剝離出來的邏輯,在享元優化我們的圖書類期間。
// 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 );
}
};
})();
結果是,原來從Book類中提取的數據,被單獨的存儲在BookManager類的一個屬性之中。這種方式相比以前我們使用的大量對象是非常有效的。有關書籍校驗的方法在這里也是非常基礎的,常用來處理外部的書籍而不是內部的書籍。
這個過程確實為我們的最終解決方案加入了一點復雜度,然而這相比我們已經解決的性能問題是不值一提的。用數據說的話,如果我們有30個相同的書籍副本,我們現在僅僅需要存儲它一次。當然,每個函數都會占用內存。當我們使用享元模式的時候,這些方法僅存在一個地方(管理對象中)而不是存在于每個對象中,因此節省了內存的使用。關于以上提到的未優化的享元版本,我們僅僅是將鏈接到對函數對象的方法存儲到了它的構造函數的原型上,但是換一種實現方式,方法將被每個對象實例所創建。
享元模式與DOM
DOM(文檔對象模型)允許對象使用兩種方式觸發事件——事件冒泡或者事件捕獲(向上傳播或者向下傳播)。
在事件捕獲中,事件首先被最外層的元素捕獲并逐漸的向內層傳播。在事件冒泡中,事件由內層元素向外層傳播。
在這種上下文之中最好的描述享元的隱喻,是由 Gary Chisholm 撰寫的,并且有點像這些:
把享元想象成一個池塘,一個魚張開它的嘴(此處類比事件發生),氣泡上升到水面(冒泡),當氣泡上升到水面時(行為),一個蒼蠅飛走了。在這個例子中,我們可以輕易的移動這個魚,讓它張開嘴到一個被點擊的按鈕上,這個氣泡開始逐漸飄起,蒼蠅最終飛離到被運行的函數上。
冒泡被用來處理一種場景,即單個的事件可能需要被多個定義在DOM樹中不同層級的處理程序處理。當它發生時候,事件冒泡首先從離它指定的最近的層級元素開始執行,從那里開始,事件開始傳播到它的包含元素中,直到最高的一層。
享元可以調整事件冒泡的深入,這將在稍后被我們所看到。
案例1:集中事件處理
對于我們實際的第一個案例,想象我們有一堆類似的元素在我們的文檔中,并且含有相似的執行行為,當我們的用戶行為(例如鼠標點擊或者移入)被執行時。
通常我們構造我們的手風琴組件時,菜單或者其他基于列表的控件中的每個項目都被綁定一個點擊事件,被包含在他們的父容器中。我們可以輕易的附加一個享元在我們的頂級容器中以監聽來自內部元素傳來的事件,而不是為多個字元素綁定點擊事件。然后這些邏輯可以被簡單的或者根據需要復雜的處理。
由于所提到的組件每個部分大都有相同的標記。這里有一個很好的機會,每個可能被點擊的元素都是十分相似的并且其周圍有著相似的樣式類。我們將在下面利用這些信息通過享元構建一個基本的手風琴。
一個StateManager命名空間用來封裝我們的享元邏輯,同時jQuery被用來對容易進行事件綁定初始化。為了確保這個頁面沒有其他的類似處理容器的綁定邏輯,一個解除綁定的事件首先被應用。
為了準確的確定在容器中的子元素是如何被點擊的,我們利用目標檢查法,即提供的一個被元素被點擊元素的引用,而不管它的父元素。我們使用這些信息處理點擊事件,而不是為頁面上每一個指定的子元素綁定事件。
<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="http://www.map-generator.net/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", "div.toggle", function ( e ) {
self.handleClick( e.target );
});
},
handleClick: function ( elem ) {
elem.find( "span" ).toggle( "slow" );
}
};
這里的好處是,我們把許多獨立的行動變成一個共享的行動(可能節省內存)。
例2:使用享元模式進行性能優化
在我們的第二個案例中,我們將涉及一些更深層次的性能優化,可以通過使用享元配合jQuery完成。
James Padolsey曾寫過一篇文章叫76字節更快的jQuery,他提醒我們,每一次的jQuery觸發回調,不管類型(過濾器,每個事件處理程序),我們能夠訪問函數的上下文(相關的DOM元素)通過this關鍵詞。
不幸的是,我們中的許多人已經習慣于包裝this通過$()或jquery(),這意味著每次都會構建一個新的不必要實例,而不是僅僅是表面上做的這么簡單:
$("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 );
});
杰姆斯曾想在以下情況下使用jQuery的jquery.text,然而他不同意的是,一個新的jquery對象必須在每次迭代中創建:
$( "a" ).map( function () {
return $( this ).text();
});
jQuery的工具方法在某些方面封裝是多余的,使用jQuery.methodName就好過使用jQuery.fn.methodName(例如jQuery.text與jQuery.fn.text) ,此處methodName代表一個工具方法,例如each()或者text。這避免調用更高層次抽象方法的需要,或者每次調用方法的時候構造一個新的jQuery對象,就像jQuery.methodName是類庫在使用一個更低層次的方法jQuery.fn.methodName的力量。
然而不是所有的jQuery方法都有相應的單節點功能,Padolsey構想并設計了一個工具方法jquery.single。
這里的想法是一個jQuery對象的創建和使用通過每次調用jquery.single(有效的意思是有史以來只有一個jQuery對象)。該方法的實現可以在下面找到和我們合并多個可能的對象數據到一個更重要的奇異結構,這在技術上也是一個享元。
jQuery.single = (function( o ){
var collection = jQuery([1]);
return function( element ) {
// Give collection the element:
collection[0] = element;
// Return the collection:
return collection;
};
})();
一個與實際使用相關的是:
$( "div" ).on( "click", function () {
var html = jQuery.single( this ).next().html();
console.log( html );
});
備注:雖然我們可能相信簡單的緩存我們的jq代碼就可以獲得相等的性能提升,Padolsey聲稱 $.single()仍然是值得使用的并且可以獲得更好的性能。這不是說,根本不要使用緩存,而是留意這種方法是有用的。