JavaScript 設(shè)計模式之享元模式

2018-07-28 17:48 更新

介紹

享元模式(Flyweight),運行共享技術(shù)有效地支持大量細(xì)粒度的對象,避免大量擁有相同內(nèi)容的小類的開銷(如耗費內(nèi)存),使大家共享一個類(元類)。

享元模式可以避免大量非常相似類的開銷,在程序設(shè)計中,有時需要生產(chǎn)大量細(xì)粒度的類實例來表示數(shù)據(jù),如果能發(fā)現(xiàn)這些實例除了幾個參數(shù)以外,開銷基本相同的 話,就可以大幅度較少需要實例化的類的數(shù)量。如果能把那些參數(shù)移動到類實例的外面,在方法調(diào)用的時候?qū)⑺麄儌鬟f進(jìn)來,就可以通過共享大幅度第減少單個實例 的數(shù)目。

那么如果在JavaScript中應(yīng)用享元模式呢?有兩種方式,第一種是應(yīng)用在數(shù)據(jù)層上,主要是應(yīng)用在內(nèi)存里大量相似的對象上;第二種是應(yīng)用在DOM層上,享元可以用在中央事件管理器上用來避免給父容器里的每個子元素都附加事件句柄。

享元與數(shù)據(jù)層

Flyweight中有兩個重要概念--內(nèi)部狀態(tài)intrinsic和外部狀態(tài)extrinsic之分,內(nèi)部狀態(tài)就是在對象里通過內(nèi)部方法管理,而外部信息可以在通過外部刪除或者保存。

說白點,就是先捏一個的原始模型,然后隨著不同場合和環(huán)境,再產(chǎn)生各具特征的具體模型,很顯然,在這里需要產(chǎn)生不同的新對象,所以Flyweight模式中常出現(xiàn)Factory模式,F(xiàn)lyweight的內(nèi)部狀態(tài)是用來共享的,F(xiàn)lyweight factory負(fù)責(zé)維護(hù)一個Flyweight pool(模式池)來存放內(nèi)部狀態(tài)的對象。

使用享元模式

讓我們來演示一下如果通過一個類庫讓系統(tǒng)來管理所有的書籍,每個書籍的元數(shù)據(jù)暫定為如下內(nèi)容:

ID
Title
Author
Genre
Page count
Publisher ID
ISBN

我們還需要定義每本書被借出去的時間和借書人,以及退書日期和是否可用狀態(tài):

checkoutDate
checkoutMember
dueReturnDate
availability

因為book對象設(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);
 }
};

程序剛開始可能沒問題,但是隨著時間的增加,圖書可能大批量增加,并且每種圖書都有不同的版本和數(shù)量,你將會發(fā)現(xiàn)系統(tǒng)變得越來越慢。幾千個book對象在內(nèi)存里可想而知,我們需要用享元模式來優(yōu)化。

我們可以將數(shù)據(jù)分成內(nèi)部和外部兩種數(shù)據(jù),和book對象相關(guān)的數(shù)據(jù)(title, author 等)可以歸結(jié)為內(nèi)部屬性,而(checkoutMember, dueReturnDate等)可以歸結(jié)為外部屬性。這樣,如下代碼就可以在同一本書里共享同一個對象了,因為不管誰借的書,只要書是同一本書,基本信息是一樣的:

/享元模式優(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;
};

定義基本工廠

讓我們來定義一個基本工廠,用來檢查之前是否創(chuàng)建該book的對象,如果有就返回,沒有就重新創(chuàng)建并存儲以便后面可以繼續(xù)訪問,這確保我們?yōu)槊恳环N書只創(chuàng)建一個對象:

/ 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{
               / 如果沒有,就創(chuàng)建一個,然后保存/
               var book = new Book(title, author, genre,pageCount,publisherID,ISBN);
               existingBooks[ISBN] =  book;
               return book;
           }
       }
   }
});

管理外部狀態(tài)

外部狀態(tài),相對就簡單了,除了我們封裝好的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);
   }
 };
});

通過這種方式,我們做到了將同一種圖書的相同信息保存在一個bookmanager對象里,而且只保存一份;相比之前的代碼,就可以發(fā)現(xiàn)節(jié)約了很多內(nèi)存。

享元模式與DOM

關(guān)于DOM的事件冒泡,在這里就不多說了,相信大家都已經(jīng)知道了,我們舉兩個例子。

例1:事件集中管理

舉例來說,如果我們又很多相似類型的元素或者結(jié)構(gòu)(比如菜單,或者ul里的多個li)都需要監(jiān)控他的click事件的話,那就需要多每個元素進(jìn)行事件綁定,如果元素有非常非常多,那性能就可想而知了,而結(jié)合冒泡的知識,任何一個子元素有事件觸發(fā)的話,那觸發(fā)以后事件將冒泡到上一級元素,所以利用這個特性,我們可以使用享元模式,我們可以對這些相似元素的父級元素進(jìn)行事件監(jiān)控,然后再判斷里面哪個子元素有事件觸發(fā)了,再進(jìn)行進(jìn)一步的操作。

在這里我們結(jié)合一下jQuery的bind/unbind方法來舉例。

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&amp;address=london%2C%20england&amp;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);
           // 判斷是哪一個子元素
           if(target.is("div.toggle")){
               self.handleClick(target);
           }
       });
   },

   handleClick: function(elem){
       elem.find('span').toggle('slow');
   }
});

例2:應(yīng)用享元模式提升性能

另外一個例子,依然和jQuery有關(guān),一般我們在事件的回調(diào)函數(shù)里使用元素對象是會后,經(jīng)常會用到$(this)這種形式,其實它重復(fù)創(chuàng)建了新對象,因為本身回調(diào)函數(shù)里的this已經(jīng)是DOM元素自身了,我們必要必要使用如下這樣的代碼:

 

$('div').bind('click', function(){
 console.log('You clicked: ' + $(this).attr('id'));
});
// 上面的代碼,要避免使用,避免再次對DOM元素進(jìn)行生成jQuery對象,因為這里可以直接使用DOM元素自身了。
$('div').bind('click', function(){
 console.log('You clicked: ' + this.id);
});

其實,如果非要用$(this)這樣的形式,我們也可以實現(xiàn)自己版本的單實例模式,比如我們來實現(xiàn)一個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對象的創(chuàng)建。

總結(jié)

Flyweight模式是一個提高程序效率和性能的模式,會大大加快程序的運行速度.應(yīng)用場合很多:比如你要從一個數(shù)據(jù)庫中讀取一系列字符串,這些字符串中有許多是重復(fù)的,那么我們可以將這些字符串儲存在Flyweight池(pool)中。

如果一個應(yīng)用程序使用了大量的對象,而這些大量的對象造成了很大的存儲開心時就應(yīng)該考慮使用享元模式;還有就是對象的大多數(shù)狀態(tài)可以外部狀態(tài),如果刪除對象的外部狀態(tài),那么就可以用相對較少的共享對象取代很多組對象,此時可以考慮使用享元模式。

參考地址:http://www.addyosmani.com/resources/essentialjsdesignpatterns/book/#detailflyweight


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號