享元模式
享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引:
内部状态存储于对象内部。
内部状态可以被一些对象共享。
内部状态独立于具体的场景,通常不会改变。
外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。
剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。
文件上传
两种上传类型:plugin、flash
1class Upload {2 constructor(uploadType, fileName, fileSize) {3 this.uploadType = uploadType4 this.fileName = fileName5 this.fileSize = fileSize6 this.dom = null7 }89 init(id) {10 this.id = id11 this.dom = document.createElement('div')12 this.dom.innerHTML = `131415`16 this.dom.querySelector('.delFile').onclick = () => {17 this.delFile()18 }19 document.body.appendChild(this.dom)20 }2122 delFile() {23 this.dom.parentNode.removeChild(this.dom)24 }25}2627let id = 02829window.startUpload = function (uploadType, files) {30 files.foeEach(file => {31 const uploadObj = new Upload(uploadType, file.fileName, file.fileSize)32 uploadObj.init(id++)33 })34}3536startUpload('plugin', [37 {38 fileName: 'one.txt',39 fileSize: 1000,40 },41 {42 fileName: 'two.txt',43 fileSize: 3000,44 },45])4647startUpload('flash', [48 {49 fileName: 'three.png',50 fileSize: '5000',51 },52 {53 fileName: 'four.png',54 fileSize: 4000,55 },56])
可见每个文件都要有一个 Upload 对象,当上传的文件极多时,会开销极大甚至造成内存不足
可以用享元模式优化:
1class Upload {2 constructor(uploadType) {3 this.uploadType = uploadType4 }56 delFile(id) {7 uploadManager.setExternalState(id, this)8 this.dom.parentNode.removeChild(this.dom)9 }10}1112const UploadFactory = (function () {13 const createdFlyWeightObjs = {}14 return {15 create(uploadType) {16 if (createdFlyWeightObjs[uploadType]) {17 return createdFlyWeightObjs[uploadType]18 }19 return createdFlyWeightObjs[uploadType] = new Upload(uploadType)20 }21 }22})()2324const uploadManager = (function () {25 const uploadDatabase = {}2627 return {28 add(id, uploadType, fileName, fileSize) {29 const flyWeightObj = UploadFactory.create(uploadType)30 const dom = document.createElement('div')3132 dom.innerHTML = `333435`36 dom.querySelector('.delFile').onclick = () => {37 flyWeightObj.delFile(id)38 }39 document.body.appendChild(this.dom)4041 uploadDatabase[id] = {42 fileName,43 fileSize,44 dom,45 }4647 return flyWeightObj48 },4950 setExternalState(id, flyWeightObj) {51 const uploadData = uploadDatabase[id]52 for (const i in uploadData){53 flyWeightObj[i] = uploadData[i]54 }55 },56 }57})()5859let id = 06061window.startUpload = function (uploadType, files) {62 files.forEach(file => {63 uploadManager.add(id++, uploadType, file.fileName, file.fileSize)64 })65}6667// 使用同上
享元模式重构之前的代码里一共创建了 6 个 upload 对象,而通过享元模式重构之后,对象的数量减少为 2,更幸运的是,就算现在同时上传 2000 个文件,需要创建的 upload 对象数量依然是 2
很容易让人联想到 JavaScript 的原型:
1const PluginUploader = function (fileName, fileSize) {2 this.fileName = fileName3}4PluginUploader.prototype.uploadType = 'plugin' // 享元5PluginUploader.prototype.add = function () {6 // ...7}89const FlashUploader = function (fileName, fileSize) {10 this.fileName, fileSize11}12FlashUploader.prototype.uploadType = 'flash' // 享元13FlashUploader.prototype.add = function () {14 // ...15}
之前虽然只有两个 upload 对象(内部状态),但对文件的描述对象(外部状态)仍然不会减少,这里的 prototype 用于做内部状态,一共也是两个,构造函数通过传参构造出外部状态
由此用原型模式实现了享元模式,可见很多设计模式之间是相通的,而连通它们的是为了提高代码可维护性的设计原则
使用场景
一般来说,以下情况发生时便可以使用享元模式:
一个程序中使用了大量的相似对象
由于使用了大量对象,造成很大的内存开销
对象的大多数状态都可以变为外部状态
剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象
对象池
1const objectPoolFactory = function (createObjFn) {2 const objectPool = []34 return {5 create(...args) {6 const obj = objectPool.length === 0 7 ? createObjFn(...args)8 : objectPool.shift()9 return obj10 },11 recover(obj) {12 objectPool.push(obj)13 },14 }15}1617// using18const iframeFactory = objectPoolFactory(function () {19 const iframe = document.createElement('iframe')20 document.body.appendChild(iframe)21 iframe.onload = function () {22 iframe.onload = null // 防止 iframe 重复加载的 bug23 iframeFactory.recover(iframe) // iframe 加载完成之后回收节点24 }25 return iframe26})2728const iframe1 = iframeFactory.create()29iframe1.src = 'http://baidu.com'3031const iframe2 = iframeFactory.create()32iframe2.src = 'http://QQ.com'3334setTimeout(() => {35 const iframe3 = iframeFactory.create()36 iframe3.src = 'http://163.com'37}, 3000)
通过创建时的 create,onload 时的 recover,实现只用一个 iframe 对象在网页上创建多个 iframe,减小了空间上的损耗