享元模式
享元(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();
};
下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和50个女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
现在来改写一下代码,既然只需要区别男女模特,那我们先把underwear参数从构造函数中移除,构造函数只接收sex参数:
var Model = function (sex) {
this.sex = sex;
}
Model.prototype.takePhoto = function () {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
};
for (var i = 0; i < 50; i++) {
var maleModel = new Model('male');
maleModel.underwear = 'M-underwear';
maleModel.takePhoto();
}
for (var j = 0; j < 50; j++) {
var femaleModel = new Model('female');
femaleModel.underwear = 'F-underwear';
femaleModel.takePhoto();
}
可以看到,改进之后的代码,只需要两个对象便完成了同样的功能。
内部状态与外部状态
上面这个例子便是享元模式的雏形,享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。
使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态,如同不管什么样式的衣服,都可以按照性别不同,穿在同一个男模特或者女模特身上,模特的性别就可以作为内部状态储存在共享对象的内部。而外部状态取决于具体的场景,并根据场景而变化,就像例子中每件衣服都是不同的,它们不能被一些对象共享,因此只能被划分为外部状态。
享元模式的通用结构
上面的示例初步展示了享元模式的威力,但这还不是一个完整的享元模式,在这个例子中还存在以下两个问题。
- 我们通过构造函数显式new出了男女两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象。
- 给model对象手动设置了underwear外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难。
我们通过一个对象工厂来解决第一个问题,只有当某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。
文件上传的例子
对象爆炸的例子
var id = 0;
window.startUpload = function (uploadType, files) { // uploadType区分是控件还是flash
for (var i = 0, file; file = files[i++];) {
var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
uploadObj.init(id++); // 给upload对象设置一个唯一的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);
};
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);
}
};
// 调用
document.querySelector('button').addEventListener('click', e => {
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
}
]);
})
享元模式重构文件上传
可以看到,在上面常规的文件上传例子中,有多少个文件,就创建了多少个对象,对浏览器的内存消耗非常大。
可以通过享元模式改进。
把uploadType
作为内部状态。一旦明确了uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的。而fileName和fileSize是根据场景而变化的,每个文件的fileName和fileSize都不一样,fileName和fileSize没有办法被共享,它们只能被划分为外部状态。
// 剥离外部状态
var Upload = function (uploadType) {
this.uploadType = uploadType;
};
Upload.prototype.delFile = function (id) {
debugger
uploadManager.setExternalState(id, this); // 当前id对应的对象的外部状态都组装到共享对象中。
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
// 工厂进行对象实例化
var UploadFactory = (function () {
var createdFlyWeightObjs = {};
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
}
}
})();
// 管理器封装外部状态
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];
}
}
}
})();
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.warn(uploadObj)
}
};
享元模式重构之前的代码里一共创建了6个upload对象,而通过享元模式重构之后,对象的数量减少为2,更幸运的是,就算现在同时上传2000个文件,需要创建的upload对象数量依然是2。
享元模式的适用性
享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个factory对象和一个manager对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。
享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。
var toolTipFactory = (function () {
var toolTipPool = []; // toolTip对象池
return {
create: function () {
if (toolTipPool.length === 0) { // 如果对象池为空
var div = document.createElement('div'); // 创建一个dom
document.body.appendChild(div);
return div;
} else { // 如果对象池里不为空
return toolTipPool.shift(); // 则从对象池中取出一个dom
}
},
recover: function (tooltipDom) {
return toolTipPool.push(tooltipDom); // 对象池回收dom
}
}
})();
var ary = [];
for (var i = 0, str; str = ['A', 'B'][i++];) {
var toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
ary.push(toolTip);
};
for (var i = 0, toolTip; toolTip = ary[i++];) {
toolTipFactory.recover(toolTip);
};
for (var i = 0, str; str = ['A', 'B', 'C', 'D', 'E', 'F'][i++];) {
var toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
};
对象池的通用实现
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 );
}
}
};
通用对象池装载iFrame的实现
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);