享元模式
2022年9月29日大约 4 分钟
享元模式
享元(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();
};
要得到一张照片,每次都需要传入 sex 和 underwear 参数,如上所述,现在一共有 50 种男内
衣和 50 种女内衣,所以一共会产生 100 个对象。如果将来生产了 10000 种内衣,那这个程序可
能会因为存在如此多的对象已经提前崩溃。
其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
改写一下代码,既然只需要区别男女模特,那我们先把 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 个对象。
享元模式文件上传
// 工厂进行对象实例化 如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:
var UploadFactory = (function () {
var createdFlyWeightObjs = {};
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
}
}
})();
// 现在我们来完善前面提到的 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);
console.log(uploadObj);
}
};
// 最后是测试时间,运行下面的代码后,可以发现运行结果跟用享元模式重构之前一致:
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
}
]);
// 通过享元模式, 就算现在同时上传 2000个文件,需要创建的 upload 对象数量依然是 2。