深入理解JavaScript系列(37):設(shè)計(jì)模式之享元模式詳解
介紹
享元模式(Flyweight),運(yùn)行共享技術(shù)有效地支持大量細(xì)粒度的對(duì)象,避免大量擁有相同內(nèi)容的小類的開(kāi)銷(如耗費(fèi)內(nèi)存),使大家共享一個(gè)類(元類)。
享元模式可以避免大量非常相似類的開(kāi)銷,在程序設(shè)計(jì)中,有時(shí)需要生產(chǎn)大量細(xì)粒度的類實(shí)例來(lái)表示數(shù)據(jù),如果能發(fā)現(xiàn)這些實(shí)例除了幾個(gè)參數(shù)以外,開(kāi)銷基本相同的 話,就可以大幅度較少需要實(shí)例化的類的數(shù)量。如果能把那些參數(shù)移動(dòng)到類實(shí)例的外面,在方法調(diào)用的時(shí)候?qū)⑺麄儌鬟f進(jìn)來(lái),就可以通過(guò)共享大幅度第減少單個(gè)實(shí)例 的數(shù)目。
那么如果在JavaScript中應(yīng)用享元模式呢?有兩種方式,第一種是應(yīng)用在數(shù)據(jù)層上,主要是應(yīng)用在內(nèi)存里大量相似的對(duì)象上;第二種是應(yīng)用在DOM層上,享元可以用在中央事件管理器上用來(lái)避免給父容器里的每個(gè)子元素都附加事件句柄。
享元與數(shù)據(jù)層
Flyweight中有兩個(gè)重要概念--內(nèi)部狀態(tài)intrinsic和外部狀態(tài)extrinsic之分,內(nèi)部狀態(tài)就是在對(duì)象里通過(guò)內(nèi)部方法管理,而外部信息可以在通過(guò)外部刪除或者保存。
說(shuō)白點(diǎn),就是先捏一個(gè)的原始模型,然后隨著不同場(chǎng)合和環(huán)境,再產(chǎn)生各具特征的具體模型,很顯然,在這里需要產(chǎn)生不同的新對(duì)象,所以Flyweight模式中常出現(xiàn)Factory模式,F(xiàn)lyweight的內(nèi)部狀態(tài)是用來(lái)共享的,F(xiàn)lyweight factory負(fù)責(zé)維護(hù)一個(gè)Flyweight pool(模式池)來(lái)存放內(nèi)部狀態(tài)的對(duì)象。
使用享元模式
讓我們來(lái)演示一下如果通過(guò)一個(gè)類庫(kù)讓系統(tǒng)來(lái)管理所有的書籍,每個(gè)書籍的元數(shù)據(jù)暫定為如下內(nèi)容:
ID
Title
Author
Genre
Page count
Publisher ID
ISBN
我們還需要定義每本書被借出去的時(shí)間和借書人,以及退書日期和是否可用狀態(tài):
checkoutDate
checkoutMember
dueReturnDate
availability
因?yàn)閎ook對(duì)象設(shè)置成如下代碼,注意該代碼還未被優(yōu)化:
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;
},
/*其它get方法在這里就不顯示了*/
// 更新借出狀態(tài)
updateCheckoutStatus: function(bookID, newStatus, checkoutDate,checkoutMember, newReturnDate){
this.id = bookID;
this.availability = newStatus;
this.checkoutDate = checkoutDate;
this.checkoutMember = checkoutMember;
this.dueReturnDate = newReturnDate;
},
//續(xù)借
extendCheckoutPeriod: function(bookID, newReturnDate){
this.id = bookID;
this.dueReturnDate = newReturnDate;
},
//是否到期
isPastDue: function(bookID){
var currentDate = new Date();
return currentDate.getTime() > Date.parse(this.dueReturnDate);
}
};
程序剛開(kāi)始可能沒(méi)問(wèn)題,但是隨著時(shí)間的增加,圖書可能大批量增加,并且每種圖書都有不同的版本和數(shù)量,你將會(huì)發(fā)現(xiàn)系統(tǒng)變得越來(lái)越慢。幾千個(gè)book對(duì)象在內(nèi)存里可想而知,我們需要用享元模式來(lái)優(yōu)化。
我們可以將數(shù)據(jù)分成內(nèi)部和外部?jī)煞N數(shù)據(jù),和book對(duì)象相關(guān)的數(shù)據(jù)(title, author 等)可以歸結(jié)為內(nèi)部屬性,而(checkoutMember, dueReturnDate等)可以歸結(jié)為外部屬性。這樣,如下代碼就可以在同一本書里共享同一個(gè)對(duì)象了,因?yàn)椴还苷l(shuí)借的書,只要書是同一本書,基本信息是一樣的:
/*享元模式優(yōu)化代碼*/
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;
};
定義基本工廠
讓我們來(lái)定義一個(gè)基本工廠,用來(lái)檢查之前是否創(chuàng)建該book的對(duì)象,如果有就返回,沒(méi)有就重新創(chuàng)建并存儲(chǔ)以便后面可以繼續(xù)訪問(wèn),這確保我們?yōu)槊恳环N書只創(chuàng)建一個(gè)對(duì)象:
/* Book工廠 單例 */
var BookFactory = (function(){
var existingBooks = {};
return{
createBook: function(title, author, genre,pageCount,publisherID,ISBN){
/*查找之前是否創(chuàng)建*/
var existingBook = existingBooks[ISBN];
if(existingBook){
return existingBook;
}else{
/* 如果沒(méi)有,就創(chuàng)建一個(gè),然后保存*/
var book = new Book(title, author, genre,pageCount,publisherID,ISBN);
existingBooks[ISBN] = book;
return book;
}
}
}
});
管理外部狀態(tài)
外部狀態(tài),相對(duì)就簡(jiǎn)單了,除了我們封裝好的book,其它都需要在這里管理:
/*BookRecordManager 借書管理類 單例*/
var BookRecordManager = (function(){
var bookRecordDatabase = {};
return{
/*添加借書記錄*/
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);
}
};
});
通過(guò)這種方式,我們做到了將同一種圖書的相同信息保存在一個(gè)bookmanager對(duì)象里,而且只保存一份;相比之前的代碼,就可以發(fā)現(xiàn)節(jié)約了很多內(nèi)存。
享元模式與DOM
關(guān)于DOM的事件冒泡,在這里就不多說(shuō)了,相信大家都已經(jīng)知道了,我們舉兩個(gè)例子。
例1:事件集中管理
舉例來(lái)說(shuō),如果我們又很多相似類型的元素或者結(jié)構(gòu)(比如菜單,或者ul里的多個(gè)li)都需要監(jiān)控他的click事件的話,那就需要多每個(gè)元素進(jìn)行事件綁定,如果元素有非常非常多,那性能就可想而知了,而結(jié)合冒泡的知識(shí),任何一個(gè)子元素有事件觸發(fā)的話,那觸發(fā)以后事件將冒泡到上一級(jí)元素,所以利用這個(gè)特性,我們可以使用享元模式,我們可以對(duì)這些相似元素的父級(jí)元素進(jìn)行事件監(jiān)控,然后再判斷里面哪個(gè)子元素有事件觸發(fā)了,再進(jìn)行進(jìn)一步的操作。
在這里我們結(jié)合一下jQuery的bind/unbind方法來(lái)舉例。
HTML:
<div id="container">
<div class="toggle" href="#">更多信息 (地址)
<span class="info">
這里是更多信息
</span></div>
<div class="toggle" href="#">更多信息 (地圖)
<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>
JavaScript:
stateManager = {
fly: function(){
var self = this;
$('#container').unbind().bind("click", function(e){
var target = $(e.originalTarget || e.srcElement);
// 判斷是哪一個(gè)子元素
if(target.is("div.toggle")){
self.handleClick(target);
}
});
},
handleClick: function(elem){
elem.find('span').toggle('slow');
}
});
例2:應(yīng)用享元模式提升性能
另外一個(gè)例子,依然和jQuery有關(guān),一般我們?cè)谑录幕卣{(diào)函數(shù)里使用元素對(duì)象是會(huì)后,經(jīng)常會(huì)用到$(this)這種形式,其實(shí)它重復(fù)創(chuàng)建了新對(duì)象,因?yàn)楸旧砘卣{(diào)函數(shù)里的this已經(jīng)是DOM元素自身了,我們必要必要使用如下這樣的代碼:
$('div').bind('click', function(){
console.log('You clicked: ' + $(this).attr('id'));
});
// 上面的代碼,要避免使用,避免再次對(duì)DOM元素進(jìn)行生成jQuery對(duì)象,因?yàn)檫@里可以直接使用DOM元素自身了。
$('div').bind('click', function(){
console.log('You clicked: ' + this.id);
});
其實(shí),如果非要用$(this)這樣的形式,我們也可以實(shí)現(xiàn)自己版本的單實(shí)例模式,比如我們來(lái)實(shí)現(xiàn)一個(gè)jQuery.signle(this)這樣的函數(shù)以便返回DOM元素自身:
jQuery.single = (function(o){
var collection = jQuery([1]);
return function(element) {
// 將元素放到集合里
collection[0] = element;
// 返回集合
return collection;
};
});
使用方法:
$('div').bind('click', function(){
var html = jQuery.single(this).next().html();
console.log(html);
});
這樣,就是原樣返回DOM元素自身了,而且不進(jìn)行jQuery對(duì)象的創(chuàng)建。
總結(jié)
Flyweight模式是一個(gè)提高程序效率和性能的模式,會(huì)大大加快程序的運(yùn)行速度.應(yīng)用場(chǎng)合很多:比如你要從一個(gè)數(shù)據(jù)庫(kù)中讀取一系列字符串,這些字符串中有許多是重復(fù)的,那么我們可以將這些字符串儲(chǔ)存在Flyweight池(pool)中。
如果一個(gè)應(yīng)用程序使用了大量的對(duì)象,而這些大量的對(duì)象造成了很大的存儲(chǔ)開(kāi)心時(shí)就應(yīng)該考慮使用享元模式;還有就是對(duì)象的大多數(shù)狀態(tài)可以外部狀態(tài),如果刪除對(duì)象的外部狀態(tài),那么就可以用相對(duì)較少的共享對(duì)象取代很多組對(duì)象,此時(shí)可以考慮使用享元模式。
- javascript 設(shè)計(jì)模式之享元模式原理與應(yīng)用詳解
- JavaScript設(shè)計(jì)模式之享元模式實(shí)例詳解
- js設(shè)計(jì)模式之結(jié)構(gòu)型享元模式詳解
- 總結(jié)JavaScript設(shè)計(jì)模式編程中的享元模式使用
- 學(xué)習(xí)JavaScript設(shè)計(jì)模式之享元模式
- javascript設(shè)計(jì)模式 – 外觀模式原理與用法實(shí)例分析
- javascript設(shè)計(jì)模式 – 裝飾模式原理與應(yīng)用實(shí)例分析
- javascript設(shè)計(jì)模式 – 組合模式原理與應(yīng)用實(shí)例分析
- javascript設(shè)計(jì)模式 – 橋接模式原理與應(yīng)用實(shí)例分析
- JS中間件設(shè)計(jì)模式的深入探討與實(shí)例分析
- javascript設(shè)計(jì)模式 – 享元模式原理與用法實(shí)例分析
相關(guān)文章
原生js實(shí)現(xiàn)節(jié)日時(shí)間倒計(jì)時(shí)功能
本文主要分享了原生js實(shí)現(xiàn)節(jié)日時(shí)間倒計(jì)時(shí)功能的示例代碼。具有一定的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-01-01查詢json的數(shù)據(jù)結(jié)構(gòu)的8種方式簡(jiǎn)介
你有沒(méi)有對(duì)“在復(fù)雜的JSON數(shù)據(jù)結(jié)構(gòu)中查找匹配內(nèi)容”而煩惱,這篇文章介紹了查詢json的數(shù)據(jù)結(jié)構(gòu)的8種方式,總有一個(gè)適合你項(xiàng)目使用的方法2014-03-03javascript開(kāi)發(fā)技術(shù)大全 第2章 開(kāi)始JAVAScript之旅
1st JavaScript Editor ,除了有著色處,還有html標(biāo)簽、屬性、javascript事件、函數(shù),另外還可調(diào)用外部編輯來(lái)編輯網(wǎng)頁(yè),也可將常用瀏覽器內(nèi)置在窗口中。2011-07-07JavaScript中的Repaint和Reflow用法詳解
這篇文章主要介紹了JavaScript中的Repaint和Reflow用法詳解,是JS入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-07-07使用AmplifyJS組件配合JavaScript進(jìn)行編程的指南
這篇文章主要介紹了使用AmplifyJS組件配合JavaScript進(jìn)行編程的指南,AmplifyJS中提供的訂閱功能十分強(qiáng)大,需要的朋友可以參考下2015-07-07window.close(); 關(guān)閉瀏覽器窗口js代碼的總結(jié)介紹
下面小編就為大家?guī)?lái)一篇window.close(); 關(guān)閉瀏覽器窗口js代碼的總結(jié)介紹。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-07-07