享元(flyweight)模式是一種用於效能優化的模式,“fly”在這裡是蒼蠅的意思,意為蠅量級。享元模式的核心是運用共享技術來有效支援大量細粒度的物件。 如果系統中因為建立了大量類似的物件而導致記憶體佔用過高,享元模式就非常有用了。在 JavaScript 中,瀏覽器特別是移動端的瀏覽器分配的記憶體並不算多,如何節省記憶體就成了一件非常有意義的事情。
故事背景
假設有個內衣工廠,目前的產品有 50 種男式內衣和 50 種女士內衣,為了推銷產品,工廠決定生產一些塑料模特來穿上他們的內衣拍成廣告照片。 正常情況下需要 50個男模特和50個女模特,然後讓他們每人分別穿上一件內衣來拍照。
程式碼實現(未使用享元模式)
var Model = function( sex, underwear){
this.sex = sex;
this.underwear = underwear;
};
Model.prototype.takePhoto = function(){
console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
for ( var i = 1; i <= 50; i++ ){
var maleModel = new Model( 'male', 'underwear' + i );
maleModel.takePhoto();
};
for ( var j = 1; j <= 50; j++ ){
var femaleModel= new Model( 'female', 'underwear' + j );
femaleModel.takePhoto();
};
複製程式碼
思考:真的需要如此多數量的物件嗎?
如上所述,現在一共有 50 種男內 衣和 50 種女內衣,所以一共會產生 100 個物件。如果將來生產了 10000 種內衣,那這個程式可能會因為存在如此多的物件已經提前崩潰。 下面我們來考慮一下如何優化這個場景。雖然有 100 種內衣,但很顯然並不需要 50 個男 模特和 50 個女模特。其實男模特和女模特各自有一個就足夠了,他們可以分別穿上不同的內衣來拍照。
程式碼重構(享元模式)
/*只需要區別男女模特
那我們先把 underwear 引數從建構函式中 移除,建構函式只接收 sex 引數*/
var Model = function( sex ){
this.sex = sex;
};
Model.prototype.takePhoto = function(){
console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
/*分別建立一個男模特物件和一個女模特物件*/
var maleModel = new Model( 'male' ),
femaleModel = new Model( 'female' );
/*給男模特依次穿上所有的男裝,並進行拍照*/
for ( var i = 1; i <= 50; i++ ){
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
};
/*給女模特依次穿上所有的女裝,並進行拍照*/
for ( var j = 1; j <= 50; j++ ){
femaleModel.underwear = 'underwear' + j;
femaleModel.takePhoto();
};
//只需要兩個物件便完成了同樣的功能
複製程式碼
如何使用享元模式
享元模式要求將物件的屬性劃分為內部狀態與外部 狀態(狀態在這裡通常指屬性)。享元模式的目標是儘量減少共享物件的數量,關於如何劃分內部狀態和外部狀態,下面的幾條經驗提供了一些指引
- 內部狀態儲存於物件內部
- 內部狀態可以被一些物件共享
- 內部狀態獨立於具體的場景,通常不會改變
- 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享
在上面的例子中,性別是內部狀態,內衣是外部狀態,通過區分這兩種狀態,大大減少了系 統中的物件數量。通常來講,內部狀態有多少種組合,系統中便最多存在多少個物件,因為性別 通常只有男女兩種,所以該內衣廠商最多隻需要 2 個物件。
享元模式的替代方案 —— 物件池
物件池是另外一種效能優化方案,它跟享元模式有一些相似之處,但沒有分離內部狀態和外 部狀態這個過程。物件池維護一個裝載空閒物件的池子,如果需要物件的時候,不是直接 new,而是轉從物件池裡獲取。如 果物件池裡沒有空閒物件,則建立一個新的物件,當獲取出的物件完成它的職責之後, 再進入 池子等待被下次獲取。
通用物件池程式碼實現
/*通用的物件池*/
var objectPoolFactory = function( createObjFn ){
var objectPool = [];
return {
create: function(){
var obj = objectPool.length === 0 ? createObjFn.apply( this, arguments ) : objectPool.shift();
return obj;
},
recover: function( obj ){
objectPool.push( obj );
}
}
};
var iframeFactory = objectPoolFactory( function(){
var iframe = document.createElement( 'iframe' );
document.body.appendChild( iframe );
iframe.onload = function(){
iframe.onload = null; // 防止 iframe 重複載入的 bug
iframeFactory.recover( iframe );// iframe 載入完成之後回收節點
}
return iframe;
});
var iframe1 = iframeFactory.create();
iframe1.src = 'http:// baidu.com';
var iframe2 = iframeFactory.create();
iframe2.src = 'http:// QQ.com';
setTimeout(function(){
var iframe3 = iframeFactory.create();
iframe3.src = 'http:// 163.com';
}, 3000 );
複製程式碼