享元( flyweight)模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象;
9 享元模式
享元( flyweight)模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象;
9.1 享元模式简单示例
假设目前加工好了50件男士外套和50件女士外套,需要使用塑料模特拍照,正常情况下需要 50 个男模特和 50 个女模特,然后让他们每人分别穿上一件外套来拍照。不使用享元模式的情况下,在程序里也许会这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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 maleModel = new Model( 'male', 'underwear' + i ); maleModel.takePhoto(); }; for ( var j = 1; j <= 50; j++ ){ var femaleModel= new Model( 'female', 'underwear' + j ); femaleModel.takePhoto(); };
|
考虑一下如何优化这个场景,其实男模特和女模特各自有一个就足够,代码调整如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| var Model = function( sex ){ this.sex = sex; }; Model.prototype.takePhoto = function(){ console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear); }; var maleModel = new Model( 'male' ), femaleModel = new Model( 'female' ); for ( var i = 1; i <= 50; i++ ){ maleModel.underwear = 'underwear' + i; maleModel.takePhoto(); }; for ( var j = 1; j <= 50; j++ ){ femaleModel.underwear = 'underwear' + j; femaleModel.takePhoto(); };
|
9.2 内部状态与外部状态
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性),享元模式的目标是尽量减少共享对象的数量;
1.如何划分内部状态和外部状态:
- 内部状态存储于对象内部;
- 内部状态可以被一些对象共享;
- 内部状态独立于具体的场景,通常不会改变;
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享;
这样便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象;组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,因此享元模式是一种用时间换空间的优化模式;
- 分析上面的例子:
在上面的例子中,性别是内部状态,外套是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女两种,所以最多只需要 2 个对象;
在上面的例子中,存在的一些问题以及解决方法:
- 通过构造函数显式
new 出了男女两个 model 对象,在其他系统中,并不是一开始就需要所有的共享对象;因此通过一个对象工厂来解决,只有当某种共享对象被真正需要时,它才从工厂中被创建出来;
- 给
model 对象手动设置了 underwear 外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难;因此用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来;
9.3 文件上传的例子
实现多个文件的上传,上传成功文件后展示文件的信息,并支持删除文件的功能;
- 文件上传基本实现代码:
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
| 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 ); };
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 ); } };
var id = 0; window.startUpload = function( uploadType, files ){ for ( var i = 0, file; file = files[ i++ ]; ){ var 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 } ]);
|
该方式的文件上传中,若一次性上传很多个文件时,每一个文件对应一个上传对象,这种对象爆炸的问题会使得浏览器崩溃;
- 享元模式重构文件上传:
在文件上传的例子里, upload 对象必须依赖 uploadType 属性才能工作,这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,因此 uploadType 作为内部状态,把其他的外部状态从构造函数中抽离出来,Upload 构造函数中只保留 uploadType 参数;
1 2 3
| var Upload = function( uploadType){ this.uploadType = uploadType; };
|
同时 Upload.prototype.init 函数也不再需要,因为 upload 对象初始化的工作被放在了 uploadManager.add 函数里面,接下来只需要定义 Upload.prototype.del 函数即可:
1 2 3 4 5 6 7 8
| 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 ); }
|
工厂进行对象实例化:定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:
1 2 3 4 5 6 7 8 9 10 11
| var UploadFactory = (function(){ var createdFlyWeightObjs = {}; return { create: function( uploadType){ if ( createdFlyWeightObjs [ uploadType] ){ return createdFlyWeightObjs [ uploadType]; } return createdFlyWeightObjs [ uploadType] = new Upload( uploadType); } } })();
|
管理器封装外部状态: uploadManager 对象负责向 UploadFactory 提交创建对象的请求,并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态,以便在程序运行过程中给 upload 共享对象设置外部状态,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 函数:
1 2 3 4 5 6
| 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 ); } };
|
最后测试,运行下面的代码后,可以发现运行结果跟用享元模式重构之前一致:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 } ]);
|
9.4 享元模式的适用性
享元模式的适用场景:
- 一个程序中使用了大量的相似对象;
- 由于使用了大量对象,造成很大的内存开销;
- 对象的大多数状态都可以变为外部状态;
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象;
9.5 再谈内部状态和外部状态
实现享元模式的关键是把内部状态和外部状态分离开来。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象;现在来考虑两种极端的情况,即对象没有外部状态和没有内部状态的时候;
没有内部状态的享元:
没有外部状态的享元:
2.9.6 对象池
对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接 new ,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取。
对象池实现:
假设在一个地图应用中, 地图上经常会出现一些标志地名的小气泡,当搜索附近地图的时候,页面里出现了 2 个小气泡。当我再搜索附近的其他地点时,页面中出现了 6 个小气泡。按照对象池的思想,在第二次搜索开始之前,并不会把第一次创建的2 个小气泡删除掉,而是把它们放进对象池;这样在第二次的搜索结果页面里,只需要再创建 4 个小气泡而不是 6 个;
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
| var toolTipFactory = (function(){ var toolTipPool = []; return { create: function(){ if ( toolTipPool.length === 0 ){ var div = document.createElement( 'div' ); document.body.appendChild( div ); return div; }else{ return toolTipPool.shift(); } }, recover: function( tooltipDom ){ return toolTipPool.push( tooltipDom ); } } })();
var ary = []; for ( var i = 0, str; str = [ 'A', 'B' ][ i++ ]; ){ var toolTip = toolTipFactory.create(); toolTip.innerHTML = str; ary.push( toolTip ); };
for ( var i = 0, toolTip; toolTip = ary[ i++ ]; ){ toolTipFactory.recover( toolTip ); };
for ( var i = 0, str; str = [ 'A', 'B', 'C', 'D', 'E', 'F' ][ i++ ]; ){ var toolTip = toolTipFactory.create(); toolTip.innerHTML = str; };
|
9.6 享元模式小结
享元模式是为解决性能问题而生的模式,在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题;
系列链接
- JavaScript 设计模式基础知识
- JavaScript 设计模式(一)单例模式
- JavaScript 设计模式(二)策略模式
- JavaScript 设计模式(三)代理模式
- JavaScript 设计模式(四)迭代器模式
- JavaScript 设计模式(五)发布订阅模式
- JavaScript 设计模式(六)命令模式
- JavaScript 设计模式(七)组合模式
- JavaScript 设计模式(八)模板方法模式
- JavaScript 设计模式(九)享元模式
- JavaScript 设计模式(十)职责链模式
- JavaScript 设计模式(十一)中介者模式
- JavaScript 设计模式(十二)装饰者模式
- JavaScript 设计模式(十三)状态模式
- JavaScript 设计模式(十四)适配器模式
- JavaScript 设计模式(下)设计原则
- JavaScript 设计模式练习代码