享元模式是一種用於性能優化的模式,核心是運用共享技術來有效支持大量細粒度的對象。
如果系統中創建了大量類似的對象,導致內存佔用過高,這時候享元模式就很有用了。
1,假髮工廠
假設有一個假髮工廠,生產50種男士假髮和50種女士假髮,這時候我們需要50個男模和50個女模,分別套上假髮,然後拍照:
var Model = function (sex, underware) {
this.sex = sex
this.underware = underware
}
Model.prototype.takePhoto = function () {
console.log('sex:' + this.sex + 'underware:' + this.underware)
}
for (var i = 0; i < 50; i++) {
var maleModel = new Model('男', 'underware' + i)
maleModel.takePhoto()
}
for (var i = 0; i < 50; i++) {
var femaleModel = new Model('女', 'underware' + i)
femaleModel.takePhoto()
}
很明顯,這種做法略顯沙雕。
2,享元模式優化
實際上,我們只需要兩個模特就夠了,假髮換着套就行。
//效果一樣,感覺好多了
var Model = function (sex) {
this.sex = sex
}
Model.prototype.takePhoto = function () {
console.log('sex:' + this.sex + 'underware:' + this.underware)
}
var maleModel = new Model('男')
var femaleModel = new Model('女')
for (var i = 0; i < 50; i++) {
maleModel.underware = 'underware' + i
maleModel.takePhoto()
}
for (var i = 0; i < 50; i++) {
femaleModel.underware = 'underware' + i
femaleModel.takePhoto()
}
3,內部狀態與外部狀態
享元模式的目標是儘量減少共享對象的數量:
(1)內部狀態存儲於對象內部
(2)內部狀態可以被一些對象共享
(3)內部狀態獨立於具體的場景
(4)外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共享
享元模式是一種時間換空間的優化模式
4,普通版本上傳文件的例子
現在我們要實現一個功能:往服務器上傳文件。假設目前只有兩種上傳方式:插件上傳和flash上傳。這倆實現是一樣的,選擇文件之後,都會調用window下的startUpload方法,用戶選擇的文件列表被組合成一個數組files放到該函數的參數列表中。
代碼如下:
var id = 0;
//uploadType區分是插件上傳還是flash
window.startUpload = function (uploadType, files) {
for (var i = 0, file; file = files[i++];) {
var upload = new Upload(uploadType, file.fileName, file.fileSize)
upload.init(id++)
}
}
用戶選擇完文件後,startUpload函數會遍歷files數組,挨個創建對應的upload對象。現在我們要定義Upload構造函數,除了初始化init函數之外,我們還需要一個刪除函數delFile。代碼如下:
// uploadType:上傳類型, fileName:文件名, fileSize:文件大小
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)
}
Upload.prototype.delFile = function () {
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('確定刪除該文件嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom)
}
}
現在,讓我們來try一try:
startUpload('plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.txt',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
}
])
startUpload('flash', [
{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.txt',
fileSize: 3000
},
{
fileName: '6.txt',
fileSize: 5000
}
])
5,享元模式優化文件上傳
上面這個上傳功能,有多少個需要上傳的對象,就創建了多少個upload對象,非常的浪費。下面,我們用享元模式來重構它。
(1)首先,要明確內部狀態
upload對象依賴於upLoadType屬性,由於我們現在是簡化版的上傳,因此只要明確了upLoadType,這個upload對象是可以被其他文件共享的。而fileName、fileSize都不一樣,因此這倆是外部狀態。
(2)剝離外部狀態
現在,我們的Upload構造函數中,只需要upLoadType這一個參數就可以:
var Upload = function (uploadType) {
this.uploadType = uploadType
}
Upload.prototype.init函數也不需要了,現在upload對象的初始化被放在了uploadManager.add這個函數中。
接下來我們只需要定義Upload.prototype.delFile函數就ok:
Upload.prototype.delFile = function (id) {
// 刪除之前,讀取文件實際大小
uploadManager.setExternalState(id, this)
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom)
}
if (window.confirm('確定刪除該文件嗎?' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom)
}
}
(3)工廠函數進行對象實例化
定義一個工廠,用來創建upload對象,如果某種內部狀態(upLoadType)的對象已經被創建過,那麼直接返回這個對象,否則創建新的對象:
var UploadFactory = (function () {
var createdFlyWeightObjs = {}
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType]
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType)
}
}
})()
(4)管理器封裝外部狀態
在(2)中我們提到了uploadManager對象,這個對象負責向UploadFactory提交創建對象的請求,並用一個uploadDatabase對象保存所有的upload對象的外部狀態,以便在程序運行中給upload共享對象設置外部狀態:
var uploadManager = (function () {
var uploadDatabase = {}
return {
add: function (id, uploadType, fileName, fileSize) {
var flyWeightObj = UploadFactory.create(uploadType)
var dom = document.createElement('div')
dom.innerHTML = `<span>文件名稱:${fileName},文件大小:${fileSize}</span><button class="delFile">刪除</button>`
dom.querySelector('.delFile').onclick = function () {
flyWeightObj.delFile(id)
}
document.body.appendChild(dom)
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom: dom
}
return flyWeightObj
},
setExternalState: function (id, flyWeightObj) {
var uploadData = uploadDatabase[id]
for (var i in uploadData) {
flyWeightObj[i] = uploadData[i]
}
}
}
})()
現在,我們要觸發startUpload 操作了:
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)
}
}
讓我們try一try最後的效果:
startUpload('plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.txt',
fileSize: 3000
},
{
fileName: '3.txt',
fileSize: 5000
}
])
startUpload('flash', [
{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.txt',
fileSize: 3000
},
{
fileName: '6.txt',
fileSize: 5000
}
])
6,小結
享元模式是一個很好的性能優化方案,但是因爲多了uploadManager和UploadFactory 對象,也會帶來維護問題。
享元模式適用場景:
- 一個程序中大量使用相似對象
- 對於使用大量對象,造成了很大的內存開銷
- 對象的大多數狀態可以變爲外部狀態
- 剝離對象的外部狀態後,可以用較少的共享對象取代大量對象