享元模式(Flyweight)
享元模式是一种用于性能优化的模式。
享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻再把外部状态组装进共享对象。
享元模式的适用性:
- 一个程序中使用了大量的相似对象
- 由于使用了大量对象,造成很大的内存开销
- 对象的大多数状态都可以变成外部状态
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象
缺点 (进阶中有举例):
- 维护多一份外部状态对象与工厂对象的开销
初识
假设目前有产品50种男生内裤和50种女生女裤,现在要生产一些塑料模特来穿上它们拍照用于广告宣传。正常情况下,需要50个男模特和50个女模特,然后分别让它们每人穿上一条内裤来拍照。
const Model = function(sex, underwear) {
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function() {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
}
for (let i = 1; i <= 50; i++ ){
var maleModel = new Model( 'male', 'underwear' + i )
maleModel.takePhoto()
}
// 女模特同上
这时我们会发现一个问题,目前一共产生了100个对象,如果将来要生产10000种呢?这程序很可能会崩溃掉。
其实男模特和女模特各要一个即可,让它们分别穿上不同的内裤拍照就好了。
const Model = function (sex) {
this.sex = sex
}
Model.prototype.takePhoto = function () {
console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
}
const maleModel = new Model('male'),
femaleModel = new Model('female')
for (let i = 1; i <= 50; i++) {
maleModel.underwear = 'underwear' + i
maleModel.takePhoto()
}
for (let j = 1; j <= 50; j++) {
femaleModel.underwear = 'underwear' + j
femaleModel.takePhoto()
}
然而这就是一个简单的享元模式雏形。
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。
在上面的例子,性别就是内部状态,内衣就是外部状态。
享元模式的目标是尽量减少共享对象的数量。
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然会消耗一定时间,却可以大大减少系统中的对象数量。因此,享元模式是一种用时间换空间的优化模式。
进阶
在上例子,问题一,我们通过构造函数显示 new
出两个对象,但在其他系统可能不是一开始就需要的。问题二,我们手动设置 underwear
外部状态,这是基于外部状态比较简单的情况下,如果外部状态相当复杂,则与共享对象的联系就会变得困难。
解决方法:
- 使用对象工厂,只有当某种共享对象被真正需要时,才从工厂被创建出来。
- 用管理器来记录对象相关的外部状态。
例:现在有一个上传功能,支持同时选择2000个文件,同时支持3种上传方式:插件上传、Flash上传、表单上传。
分析:如果在不使用模式情况下,同时 new
2000个上传对象出来,往往浏览器就出现假死状态。
第一步剥离外部状态
const Upload = function (uploadType) {
this.uploadType = uploadType
}
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)
}
}
第二步工厂进行对象实例化
const UploadFactory = (function () {
const createFlyWeightObjs = {}
return {
create: function (uploadType) {
if (createFlyWeightObjs[uploadType]) {
return createFlyWeightObjs[uploadType]
}
return createFlyWeightObjs[uploadType] = new Upload(uploadType)
}
}
})()
第三步管理器封装外部状态
const uploadManager = (function () {
const uploadDatabase = {}
return {
add: function (id, uploadType, fileName, fileSize) {
const flyWeightObj = UploadFactory.create(uploadType)
const 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) {
const uploadData = uploadDatabase[id]
for (let i in uploadData) {
// 传入外部状态
flyWeightObj[i] = uploadData[i]
}
}
}
})()
第四步触发
let id = 0
window.startUpload = function (uploadType, files) {
for (let i = 0, file; file = files[i++];) {
const uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
}
}
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
}
])
实现效果:
现在通过享元模式实现,对象的数量控制为2,如果还是这两种上传模式,即使同时上传3000个文件,还是只创建2个。即有多少种内部状态,就最多存在多少个共享对象。
延伸
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。
比如:HTTP连接池和数据库连接池,在web开发中,对象池使用最多的场景大概是跟DOM有关的操作。
比如:现在地图软件在你搜索某个地点后,会在相关地点标上小气泡,一开始你搜xx饭店,可能页面上会出现3个小气泡,而再搜索菜鸟驿站的时候周围出现10个小气泡。在对象池的思想里,第一次会创建3个小气泡对象,但在第二次搜索前,不会把这3个小气泡给删除掉,而是回收到对象池,在第二次搜索后,只需再创建7个小气泡即可。
通用对象池
const objectPoolFactory = function (createObjFn) {
let objectPool = []
return {
// 判断对象池中是否有空闲对象,有则直接使用
create: function () {
const obj = objectPool.lenght === 0 ? createObjFn.apply(this, arguments) : objectPool.shift()
return obj
},
// 回收对象
recover: function () {
objectPool.push(ohj)
}
}
}
// 该方法具体需求具体实现
const iframeFactory = objectPoolFactory(function () {
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
iframe.onload = function () {
iframe.onload = null
iframeFactory.recover(iframe)
}
return iframe
})
const iframe1 = iframeFactory.create()
iframe1.src = ''
const iframe2 = iframeFactory.create()
iframe2.src = ''
setTimeout(function () {
const iframe3 = iframeFactory.create()
iframe3.src = ''
}, 3000)