享元模式解决的痛点是当一个操作需要创建无数多个对象时,对象数量过多的问题。
如果我们可以在这个操作中提取出内部对象和外部对象,就能在只创建等于内部对象个数的对象的情况下,实现相同的目标。
一个例子是,假设一个服装厂有新款男装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 model = new Model('male', i);
model.takePhoto();
}
for (var i = 1; i < 50; i++) {
var model = new Model('female', i);
model.takePhoto();
}
这种场景是:叫了100个模特试穿了100件衣服,程序一共创建了100个对象。如果要试穿的是1000件衣服,无疑产生的内存开销是巨大的。
实际上,我们只需要一男一女两个模特,每个人穿50件衣服即可。
在这个过程中,可以被重复使用的模特就是内部状态,不可复用的衣服就是外部状态。
这两者的区别是:
- 内部状态存储于对象内部;
- 内部状态可以被一些对象共享;
- 内部状态独立于具体的场景,通常不会改变;
- 外部状态取决于具体的场景,并根据场景变化,外部状态不能被共享。
享元模式的实际应用——文件上传
1. 对象爆炸
在做文件上传功能时,虽然可以按照队列一个一个地排队上传,但也支持同时选择2000个文件同时上传。每一个文件都对应着一个 JS 上传对象的创建。如果同时 new 2000个对象,显然是不太合理的。
此外,上传还会支持几种上传方式,比如插件上传,flash上传等,这里我们只以插件上传和 flash 上传为例,当用户选择了上传模式后,插件和flash都会通知调用 Window 下的一个全局 JavaScript 函数——startUpload,用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里。代码如下:
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);
};
接下来实现文件的删除功能,当被删除的文件小于3000KB时可以直接删除,否则弹出一个提示框,提示用户是否要删除该文件:
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);
}
};
接下来分别创建3个插件上传对象和3个Flash上传对象:
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
}
]);
2. 享元模式重构文件上传
上一节代码是第一版的文件上传, 有多少个需要上传的文件,就一共创建了多少个Upload对象,接下来用享元模式重构它。
首先提取内部状态和外部状态。可以发现,除了 UploadType
可以被提取为 插件上传 和 Flash 上传,其他状态都不是固定的,所以文件大小,文件名等都是外部状态。
2.1 剥离外部状态
明确了UploadType
是内部状态后,我们就要把其他状态从构造函数中抽离出来,Upload 构造函数中只保留 uploadType
参数:
var Upload = function(uploadType) {
this.uploadType = uploadType;
}
Upload.prototype.init
函数也不再需要,因为 upload 对象初始化的工作被放在了 uploadManager.add
函数里面,接下来只需要定义Upload.prototype.del
函数即可:
Upload.prototype.delFile = function (id) {
uploadManager.setExternalState(id, this); // (1)
if (this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
if (window.confirm('确定要删除该文件吗? ' + this.fileName)) {
return this.dom.parentNode.removeChild(this.dom);
}
};
在开始删除文件之前,需要读取文件的实际大小,而文件的fileSize
被储存在外部管理器uploadManager
中,所以需要uploadManager.setExternalState
方法给共享对象设置正确的fileSize
,上段的代码(1)处表示把当前 id 对应的对象的外部状态,从外部管理器中拿过来,组装到共享对象中。
工厂进行对象实例化
定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:
var UploadFactory = (function () {
var createdFlyWeightObjs = {};
return {
create: function (uploadType) {
if (createdFlyWeightObjs[uploadType]) {
return createdFlyWeightObjs[uploadType];
}
return createdFlyWeightObjs[uploadType] = new Upload(uploadType);
}
}
})();
这里是利用了闭包,把已经存在的共享对象保存在createdFlyWeightObjs
中。
管理器封装外部状态
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);
}
};
最后是测试时间,运行第一版的测试代码,可以发现运行结果跟用享元模式重构之前一致。
通过享元模式,即使需要上传的文件有2000个,实际创建的对象也只有2个(插件上传对象,flash上传对象)。
对象池
对象池可以实现对象的回收再利用。创建一个对象之后,如果不需要,可以回收进一个数组中,下次要用到的时候,再从数组中取出。避免了对象的重复创建。
网友评论