深入理解JavaScript系列(37):設計模式之享元模式

享元模式(Flyweight),運行共享技術有效地支援大量細粒度的對象,避免大量擁有相同內容的小類的開銷(如耗費記憶體),使大家共享一個類(元類)。

享元模式可以避免大量非常相似類的開銷,在程式設計中,有時需要生產大量細粒度的類實例來表示數據,如果能發現這些實例除了幾個參數以外,開銷基本相同的 話,就可以大幅度較少需要實例化的類的數量。如果能把那些參數移動到類實例的外面,在方法調用的時候將他們傳遞進來,就可以通過共享大幅度第減少單個實例 的數目。

那麼如果在JavaScript中應用享元模式呢?有兩種方式,第一種是應用在數據層上,主要是應用在記憶體里大量相似的對象上;第二種是應用在DOM層上,享元可以用在中央事件管理器上用來避免給父容器里的每個子元素都附加事件句柄。

享元與數據層

Flyweight中有兩個重要概念–內部狀態intrinsic和外部狀態extrinsic之分,內部狀態就是在對象里通過內部方法管理,而外部資訊可以在通過外部刪除或者保存。

說白點,就是先捏一個的原始模型,然後隨著不同場合和環境,再產生各具特徵的具體模型,很顯然,在這裡需要產生不同的新對象,所以Flyweight模式中常出現Factory模式,Flyweight的內部狀態是用來共享的,Flyweight factory負責維護一個Flyweight pool(模式池)來存放內部狀態的對象。

使用享元模式

讓我們來演示一下如果通過一個類庫讓系統來管理所有的書籍,每個書籍的元數據暫定為如下內容:

ID  Title  Author  Genre  Page count  Publisher ID  ISBN

複製程式碼

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

checkoutDate  checkoutMember  dueReturnDate  availability

複製程式碼

因為book對象設置成如下程式碼,注意該程式碼還未被優化:

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方法在這裡就不顯示了*/    // 更新借出狀態  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);   }  };

複製程式碼

程式剛開始可能沒問題,但是隨著時間的增加,圖書可能大批量增加,並且每種圖書都有不同的版本和數量,你將會發現系統變得越來越慢。幾千個book對象在記憶體里可想而知,我們需要用享元模式來優化。

我們可以將數據分成內部和外部兩種數據,和book對象相關的數據(title, author 等)可以歸結為內部屬性,而(checkoutMember, dueReturnDate等)可以歸結為外部屬性。這樣,如下程式碼就可以在同一本書里共享同一個對象了,因為不管誰借的書,只要書是同一本書,基本資訊是一樣的:

/*享元模式優化程式碼*/  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的對象,如果有就返回,沒有就重新創建並存儲以便後面可以繼續訪問,這確保我們為每一種書只創建一個對象:

/* Book工廠 單例 */  var BookFactory = (function(){     var existingBooks = {};     return{         createBook: function(title, author, genre,pageCount,publisherID,ISBN){         /*查找之前是否創建*/             var existingBook = existingBooks[ISBN];             if(existingBook){                     return existingBook;                 }else{                 /* 如果沒有,就創建一個,然後保存*/                 var book = new Book(title, author, genre,pageCount,publisherID,ISBN);                 existingBooks[ISBN] =  book;                 return book;             }         }     }  });

複製程式碼

管理外部狀態

外部狀態,相對就簡單了,除了我們封裝好的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對象里,而且只保存一份;相比之前的程式碼,就可以發現節約了很多記憶體。

享元模式與DOM

關於DOM的事件冒泡,在這裡就不多說了,相信大家都已經知道了,我們舉兩個例子。

例1:事件集中管理

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

在這裡我們結合一下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:應用享元模式提升性能

另外一個例子,依然和jQuery有關,一般我們在事件的回調函數里使用元素對象是會後,經常會用到$(this)這種形式,其實它重複創建了新對象,因為本身回調函數里的this已經是DOM元素自身了,我們必要必要使用如下這樣的程式碼:

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

複製程式碼

其實,如果非要用$(this)這樣的形式,我們也可以實現自己版本的單實例模式,比如我們來實現一個jQuery.signle(this)這樣的函數以便返回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元素自身了,而且不進行jQuery對象的創建。

總結

Flyweight模式是一個提高程式效率和性能的模式,會大大加快程式的運行速度.應用場合很多:比如你要從一個資料庫中讀取一系列字元串,這些字元串中有許多是重複的,那麼我們可以將這些字元串儲存在Flyweight池(pool)中。

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