享元模式
享元模式要求将对象的属性分为内部状态和外部状态,其目标是尽量减少共享对象的数量。
示例
比如我们要实现一个批量上传的功能,一个基本的版本是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| class Upload { constructor(uploadType, fileName, fileSize) { this.uploadType = uploadType this.fileName = fileName this.fileSize = fileSize this.dom = null this.id = null } init(id) { this.id = id this.dom = document.createElement('div') this.dom.innerHTML = `<span>文件名称:${this.fileName}</span><span>文件大小:${this.fileSize}</span><button class="delFile">删除</button>` this.dom.querySelector('.delFile').addEventListener('click', () => { this.delFile() }) document.body.appendChild(this.dom) } delFile() { if (this.fileSize < 3000) { return this.dom.parentNode.removeChild(this.dom) } if (window.confirm('确定要删除该文件吗? ' + this.fileName)) { return this.dom.parentNode.removeChild(this.dom); } } } let id = 0; window.startUpload = function (uploadType, files) { files.forEach(file => { const uploadObj = new Upload(uploadType, file.fileName, file.fileSize) uploadObj.init(id++) }) };
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 上传文件前就需要确定上传文件的方式 是插件还是flash还是其他。
- 外部状态 fileName fileSize 这两个都会根据文件的不同而变化,因此这两个属性是外部状态。
之后需要将外部状态和内部状态分离,Upload类中只保留内部状态,外部状态交给外部管理器进行管理。而因为不需要一开始就构造Upload实例,因此我们可以采用工厂的方式实例化对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| class Upload { constructor(uploadType) { this.uploadType = uploadType } delFile(id) { UploadManage.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); } } }
class UploadFactory { static createdFlyWeightMap = {} static create(uploadType) { if (this.createdFlyWeightMap[uploadType]) { return this.createdFlyWeightMap[uploadType] } return this.createdFlyWeightMap[uploadType] = new Upload(uploadType) } }
class UploadManage { static uploadDataBase = {}; static add(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) this.uploadDataBase[id] = { fileName, fileSize, dom } return flyWeightObj } static setExternalState(id, uploadObj) { const file = this.uploadDataBase[id] for (const key in file) { uploadObj[key] = file[key] } } } let id = 0; window.startUpload = function (uploadType, files) { files.forEach(file => { const { fileName, fileSize } = file UploadManage.add(id++, uploadType, fileName, fileSize) }) };
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 } ]);
|
可以看到现在内存中只存在两个Upload的实例,分别对应plugin和flash。

实用性
显而易见,虽然创建的Upload实例从6个减少为2个,且永远是2个,但是我们需要额外维护一个Upload的实例化工厂和一个外部状态管理器。
因此虽然他可以对性能进行很好地优化,但是需要因地制宜,在某些情况下不需要引入享元模式,否则反而增加了程序的复杂性。
在满足这些条件的情况下适合引入享元模式:
- 一个程序中使用了大量的相似对象。
- 由于使用了大量对象,造成很大的内存开销。
- 对象的大多数状态都可以变为外部状态。
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
而如果没有外部状态或者没有内部状态:
对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后再进入池子等待被下次获取。
对象池技术的应用非常广泛,HTTP 连接池和数据库连接池以及如何避免频繁创建和删除DOM。
如下实现了一个简单的dom元素复用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class objectPoolFactory { objectPool = [] constructor(createObjFn) { this.createObjFn = createObjFn } create() { const obj = this.objectPool.length === 0 ? this.createObjFn.apply(this, arguments) : this.objectPool.shift(); return obj; } recover(obj) { this.objectPool.push(obj); } } const iframeFactory = new 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 = 'http://localhost:4000/jing-jiu/'; const iframe2 = iframeFactory.create(); iframe2.src = 'http://localhost:4000/jing-jiu/archives'; setTimeout(function () { const iframe3 = iframeFactory.create(); iframe3.src = 'http://localhost:4000/jing-jiu/tags'; console.log('复用'); }, 3000);
|