享元模式(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 = 0window.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)