1 亨元模式的定義
亨元模式(flyweight)是一種效能優化的模式,fly是蒼蠅的意思,意為蠅量級。亨元模式核心在於運用共享技術來有效地支援大量細粒度的物件。
如果系統中出現了大量類似的物件而導致記憶體佔用過高,亨元模式就比較有用了。
2 初識亨元模式
假設內衣廠,目前生產了50件男士T恤和50件女士T恤。現在要請模特穿上T恤並拍照。不使用亨元模式情況下,每件衣服都需要1個模特。
// 模特類
var Model = function(sex, tShirt) {
this.sex = sex;
this.tShirt = tShirt;
}
Model.prototype.takePhoto = function() {
console.log('sex:' + this.sex + ", tShrit: " + this.tShirt);
}
for ( var i = 1; i <= 50; i++ ){
var maleModel = new Model( 'male', 'tShrit' + i );
maleModel.takePhoto();
};
for ( var j = 1; j <= 50; j++ ){
var femaleModel= new Model( 'female', 'tShrit' + j );
femaleModel.takePhoto();
};
複製程式碼
在這裡建立了100個Model物件。但是實際情況我們只需要找一個男模特和一個女模特就夠了。男模特穿50件T恤拍照,女模特穿50件T恤拍照。只需要2個物件便能夠完成同樣的功能。
var Model = function(sex) {
this.sex = sex;
this.tShirt = null;
}
Model.prototype.takePhoto = function() {
console.log('sex:' + this.sex + ", tShrit: " + this.tShirt);
}
var maleModel = new Model( 'male');
for ( var i = 1; i <= 50; i++ ){
maleModel.tShirt = 'tShrit' + j;
maleModel.takePhoto();
};
var femaleModel= new Model( 'female');
for ( var j = 1; j <= 50; j++ ){
femaleModel.tShirt = 'tShrit' + j;
femaleModel.takePhoto();
};
複製程式碼
3 內部狀態與外部狀態
上面的例子中有了亨元模式的雛形,亨元模式要求將物件的屬性劃分為內部狀態和外部狀態。亨元模式的目標旨在減少共享物件的數量,關於如何劃分內部狀態和外部狀態,下面有點經驗指引:
- 內部狀態儲存於物件內部
- 內部狀態可以被一些物件共享
- 內部狀態獨立於具體的場景,通常不會改變
- 外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享 這樣我們可以把所有內部狀態(性別)相同的物件(例如性別為女的一類50個人)指定為同一個共享獨享(最終只需要以一個人),外部物件可以從物件上脫離出來(T恤被脫離),並儲存於外部。
- 剝離了外部狀態(T恤)的物件成為共享物件(maleModel),外部狀態在必要時被傳入共享物件然後組裝成為一個完整的物件。雖然耗時,但是減少了記憶體佔用,亨元模式就是以時間換空間的優化模式。
4 利用亨元模式檔案上傳
1小結
中的亨元模式例子存在兩個問題:
- 通過建構函式new出來的男女兩個model物件,在其他系統中,也許並不不是一開始就需要所有的共享物件
- 給model手動設定的外部狀態,在複雜的系統中,這不是一個好的方式,因為外部物件可能相當複雜,它們與共享物件的聯絡變的困難。
解決第一個問題:可以使用物件工廠的方式來建立,當真正需要的時候才在工廠物件建立出來。
解決第二個問題:用一個管理器來記錄物件相關的外部狀態,使這些外部狀態通過某個鉤子與共享物件聯絡起來。
4.1 檔案上傳問題描述
- 上傳一個檔案需要建立一個檔案對阿寧,當同一個操作需要支援多個檔案上傳時,每一個檔案需要建立一個物件,如果1000個檔案那麼就需要new 1000個物件。
- 當專案支援多種上傳方式:瀏覽器外掛上傳,Flash上傳和表單上傳。當使用者選擇上傳檔案之後,外掛或者Flash都會通知window的一個全域性javascript函式,名字叫startUpload。使用者選擇的檔案列表被組合成為一耳光陣列files塞進該函式的引數列表中。
var i = 0;
// fileType: 區分是控制元件,flash還是表單上傳
window.startUpload = function(fileType, files) {
files.map(function(file) {
var uploadFile = new Upload(uploadType, file.fileName, file.fileSize);
// 初始化檔案到頁面中
uploadFile.init(id++);
})
};
// 檔案物件接收三個引數:外掛型別,檔名,檔案大小
var Upload = function(uploadType, fileName, fileSize) {
this.uploadType = uploadType;
this.fileName = fileName;
this.fileSize = fileSize;
this.dom = null;
}
// 將檔案初始化到介面上
Upload.prototype.init = function(id) {
var that = this;
this.id = id;
this.dom = document.createElement('div');
this.dom.innerHTML = '<span>檔名稱:'+ this.fileName +', 檔案大小: '+ this.fileSize +'</span>' + '<button class="delFile">刪除</button>';
this.dom.querySelector('.delFile').onclick = function() {
that.delFile();
}
document.body.appendChild(this.dom);
};
// 檔案如果小於3000直接刪除,否則給與提示
Upload.prototype.delFile = function() {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
} else {
if (window.confirm('你確定要刪除檔案嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}
複製程式碼
下面上分別建立三個外掛物件和三個flash物件。每個檔案都被建立了一個Upload物件。總共6個物件。
startUpload( 'plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.html',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
}
]);
startUpload( 'flash', [
{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.html',
fileSize: 3000
},
{
fileName: '6.txt',
fileSize: 5000
}
]);
複製程式碼
4.2 亨元模式完成檔案上傳
第一版本的檔案上傳功能,需要上傳多少個檔案,就需要建立多少個物件。我們利用亨元模式的來重構。
- 找到內部狀態(儲存於物件內部,能被多個物件共享,獨立於具體場景不被改變):上傳型別即為內部狀態
- 找到外部狀態(取決於具體場景,根據場景變化而變化,不能被共享):檔案大小和檔名稱
- 使用工廠方法建立上傳物件(同意上傳型別只需要建立一個物件)
- 管理器封裝外部狀態
// 只有uploadType才是內部狀態
var Uplaod = function(uploadType) {
this.uploadType = uploadType;
}
// 檔案如果小於3000直接刪除,否則給與提示
Upload.prototype.delFile = function() {
// 獲取自己的屬性(fileSize,fileName)
uploadManager.setExternalState( id, this);
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
} else {
if (window.confirm('你確定要刪除檔案嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
}
}
// 工廠進行物件例項化
var UploadFactory = function() {
var createFlyWeight = {};
return {
create: function(uploadType) {
if (!(uploadType in createFlyWeight)) {
createFlyWeight[uploadType] = new Upload(uploadType);
}
return createFlyWeight[uploadType];
}
};
};
// 管理器封裝外部狀態
var uploadManager = (function() {
var uploadDatabase = {};
return {
add: function(id, uploadType, fileName, fileSize) {
var flyWight = UploadFactory.create(uploadType);
this.dom = document.createElement('div');
this.dom.innerHTML = '<span>檔名稱:'+ this.fileName +', 檔案大小: '+ this.fileSize +'</span>' + '<button class="delFile">刪除</button>';
this.dom.querySelector('.delFile').onclick = function() {
that.delFile();
}
document.body.appendChild(this.dom);
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom: dom
};
return flyWight;
},
setExternalState: function(id, flyWight) {
var uploadData = uploadDatabase[ id ];
for ( var i in uploadData ){
flyWight[ i ] = uploadData[ i ];
}
}
}
})();
// 呼叫
var id = 0;
window.startUpload = function( uploadType, files ){
for ( var i = 0, file; file = files[ i++ ]; ){
var uploadObj = uploadManager.add( ++id, uploadType, file.fileName, file.fileSize );
}
};
複製程式碼
和上面一樣的方式建立三個外掛物件和三個flash物件。最終在程式碼中只會產生2個物件
,通過工廠方法建立的。
startUpload( 'plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.html',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
}
]);
startUpload( 'flash', [
{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.html',
fileSize: 3000
},
{
fileName: '6.txt',
fileSize: 5000
}
]);
複製程式碼
5 亨元模式再談
亨元模式用於效能優化,但是需要額外維護factory和manager物件。在大部分不必要使用亨元模式的環境下,這部分開銷可以避免。亨元模式主要用於以下情況:
- 一個程式使用了大量相似的物件
- 由於使用大量物件造成記憶體開銷
- 物件的大多數狀態為外部狀態
- 剝離物件外部狀態之後,可以使用相對較少的共享物件來取代大量物件。
可以看出檔案上傳完全符合這四點。我們也知道了實現亨元模式就是講外部物件與內部物件分離出來,有多少種內部狀態的組合,系統中便最多產生多少個共享物件。而外部物件在需要的時候傳入共享物件組裝成為要給完整的物件。現在考慮一下兩種極端情況:完全沒有外部狀態 和 完全沒有內部狀態
5.1 完全沒有內部狀態
在檔案上傳的例子中,我們內部狀態是外掛和flash。因此建立了兩個物件。
- startUpload('plugin', []);
- startUpload('flash', []);
有些網站支援極速上傳(plugin),也支援普通上傳(flash)。當極速上傳不好使就使用普通上傳。但是也有很多網站並不需要做的如此複雜,只支援單一的上傳方式。那麼內部狀態uploadType
實際上就不存在。沒有了內部狀態意味著我們只需要唯一一個共享物件。改寫我們的工廠方法:
// 工廠進行物件例項化
var UploadFactory = function() {
var flyWeight;
return {
create: function() {
if (!flyWeight) {
flyWeight = new Upload();
}
return flyWeight;
}
};
};
複製程式碼
雖然生產共享物件的工廠方法程式設計了一個單例工廠,這從共享物件沒有了內部狀態的區別,但是還是有剝離狀態的過程,我們依然傾向於稱之為亨元模式。
5.2 完全沒有外部狀態
對於上面的工廠化建立的物件obj1, obj2,通過判斷obj1 === obj2
,會返回true。但是單純只有這兩個物件並不能說明是亨元模式的結果。亨元模式關鍵區分於內部狀態和外部狀態。亨元模式的過程是剝離外部狀態。把外部狀態儲存在其他地方,並在合適的時候將外部狀態組裝進入共享物件。這裡的obj1 和obj2完全是同一個物件。如果沒有外部狀態的剝離,即使有共享資料,但是不是一個純粹的亨元模式。
var obj1 = UploadFactory.create();
var obj2 = UploadFactory.create();
複製程式碼
6 物件池
物件池維護這一個裝載空閒物件的池子,如果需要物件時,不是直接new物件,而是從池子中獲取物件。如果池子中沒有空閒物件,則狀態一個新的物件,當獲取的物件完成了它的職責後,再進入池子等待下一次被獲取。
物件池的應用情況:
- HTTP連線池和資料庫連線池
- web前端開發,物件池使用最多的場景是DOM有關的操作。
6.1 物件池的實現
例如在地圖上,我們搜尋的時候,頁面會出現2個小氣泡。當搜尋蘭州拉麵時,會出現6個氣泡,當搜尋新巴克時,出現3個氣泡。這裡的氣泡就可以使用物件池實現
var ToolTipFactrory = (function() {
var toolTips = []; // 儲存toolTip物件池
return {
create: function() {
// 如果物件池不為空,取出一個返回
if (toolTips.length > 0) {
return toolTips.shift();
} else {
// 建立一個dom返回
var div = document.createElement('div');
return div;
}
},
recover: function(toolTipDom) {
toolTips.push(toolTipDom);
}
};
})();
複製程式碼
點選按鈕,建立toolTip。將上一次建立的節點共享於下一次的操作。物件池的思想與亨元模式的思想有點相似,el的innerHTML是外部狀態,而封裝內部el節點是內部狀態。但是我們沒有主動分離內部狀態和外部狀態。
- 第一次點選建立n個(隨機)
- 第二次點選,銷燬之前建立的內容,在重新根據返回的資料建立(n個,隨機)
var button = document.getElementById('id');
var arr = [];
button.onclick = function() {
// 新的一輪建立個數 > 當前個數,則在現有基礎上新增(newLength - oldLength)個
// 新的個數 < 當前個數,則在當前基礎上移除(oldLength - newLength)個
var length = parseInt(Math.random() * 10);
var differ = length - arr.length;
if (differ < 0) {
recoverToolTip(Math.abs(differ));
} else {
getToolTip(differ);
}
}
// 移除
function recoverToolTip(length) {
for(i = 0; i< length; i++) {
var el = arr.shift();
ToolTipFactrory.recover(el);
document.body.removeChild(el);
}
}
// 新增
function getToolTip(length,) {
for (var i = 0; i < length; i++) {
var el = ToolTipFactrory.create();
el.innerHTML = i + 'div';
document.body.appendChild(el);
arr.push(el);
}
}
複製程式碼
6.2 通用物件池
上面的物件池只能作用於toolTip的建立。我們可以將該物件池改造一下,實現一個通用的物件池。
var ObjPoolFactory = (function(createObjFn) {
var objectPool = [];
return {
create: function() {
if (objectPool.length === 0) {
return createObjFn.apply(this, arguments);
}
return objectPool.shift();
},
recover: function(obj){
objectPool.push(obj);
}
}
})();
複製程式碼