JavaScript|JavaScript 设计模式(中)——9.享元模式

9 享元模式
享元( flyweight)模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象;
9.1 享元模式简单示例 假设目前加工好了50件男士外套和50件女士外套,需要使用塑料模特拍照,正常情况下需要 50 个男模特和 50 个女模特,然后让他们每人分别穿上一件外套来拍照。不使用享元模式的情况下,在程序里也许会这样写:

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(); };

考虑一下如何优化这个场景,其实男模特和女模特各自有一个就足够,代码调整如下:
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 内部状态与外部状态 享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性),享元模式的目标是尽量减少共享对象的数量;
【JavaScript|JavaScript 设计模式(中)——9.享元模式】1.如何划分内部状态和外部状态:
  • 内部状态存储于对象内部;
  • 内部状态可以被一些对象共享;
  • 内部状态独立于具体的场景,通常不会改变;
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享;
这样便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象;组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,因此享元模式是一种用时间换空间的优化模式;
  1. 分析上面的例子:
在上面的例子中,性别是内部状态,外套是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女两种,所以最多只需要 2 个对象;
在上面的例子中,存在的一些问题以及解决方法:
  • 通过构造函数显式 new 出了男女两个 model 对象,在其他系统中,并不是一开始就需要所有的共享对象;因此通过一个对象工厂来解决,只有当某种共享对象被真正需要时,它才从工厂中被创建出来;
  • model 对象手动设置了 underwear 外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难;因此用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来;
9.3 文件上传的例子 实现多个文件的上传,上传成功文件后展示文件的信息,并支持删除文件的功能;
  1. 文件上传基本实现代码:
//定义 Upload 构造函数,它接受 3 个参数,分别是插件类型、文件名和文件大小 var Upload = function( uploadType, fileName, fileSize ){ this.uploadType = uploadType; this.fileName = fileName; this.fileSize = fileSize; this.dom= null; }; //upload 对象init函数 Upload.prototype.init = function( id ){ var that = this; this.id = id; this.dom = document.createElement( 'div' ); this.dom.innerHTML = '文件名称:'+ this.fileName +', 文件大小: '+ this.fileSize +'' + ''; this.dom.querySelector( '.delFile' ).onclick = function(){ that.delFile(); } document.body.appendChild( this.dom ); }; // upload 对象删除文件的功能 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 ); } }; // 当选择了文件并确认上传后,调用 Window 下的一个全局函数 startUpload,用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里,代码如下: var id = 0; window.startUpload = function( uploadType, files ){ // uploadType 区分是控件还是 flash for ( var i = 0, file; file = files[ i++ ]; ){ var uploadObj = new Upload( uploadType, file.fileName, file.fileSize ); uploadObj.init( id++ ); // 给 upload 对象设置一个唯一的 id } }; // 插件类型上传文件 startUpload( 'plugin', [ { fileName: '1.txt', fileSize: 1000 }, { fileName: '2.html', fileSize: 3000 }, { fileName: '3.txt', fileSize: 5000 } ]); // Flash类型上传文件 startUpload( 'flash', [ { fileName: '4.txt', fileSize: 1000 }, { fileName: '5.html', fileSize: 3000 }, { fileName: '6.txt', fileSize: 5000 } ]);

该方式的文件上传中,若一次性上传很多个文件时,每一个文件对应一个上传对象,这种对象爆炸的问题会使得浏览器崩溃;
  1. 享元模式重构文件上传:
在文件上传的例子里, upload 对象必须依赖 uploadType 属性才能工作,这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,因此 uploadType 作为内部状态,把其他的外部状态从构造函数中抽离出来,Upload 构造函数中只保留 uploadType 参数;
var Upload = function( uploadType){ this.uploadType = uploadType; };

同时 Upload.prototype.init 函数也不再需要,因为 upload 对象初始化的工作被放在了 upload?Manager.add 函数里面,接下来只需要定义 Upload.prototype.del 函数即可:
Upload.prototype.delFile = function( id ){ uploadManager.setExternalState( id, this ); // 表示把当前 id 对应的对象的外部状态都组装到共享对象中 if ( this.fileSize < 3000 ){ return this.dom.parentNode.removeChild( this.dom ); } if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){ return this.dom.parentNode.removeChild( this.dom ); }

工厂进行对象实例化:定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:
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 共享对象设置外部状态,代码如下:
var uploadManager = (function(){ var uploadDatabase = {}; return { // 创建上传文件函数 add: function( id, uploadType, fileName, fileSize ){ var flyWeightObj = UploadFactory.create( uploadType ); var dom = document.createElement( 'div' ); dom.innerHTML = '文件名称:'+ fileName +', 文件大小: '+ fileSize +'' + ''; 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 = https://www.it610.com/article/uploadDatabase[ id ]; for ( var i in uploadData ){ flyWeightObj[ i ] = uploadData[ i ]; } } } })();

接着是触发上传动作的 startUpload 函数:
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 ); } };

最后测试,运行下面的代码后,可以发现运行结果跟用享元模式重构之前一致:
// 插件类型上传文件 startUpload( 'plugin', [ { fileName: '1.txt', fileSize: 1000 }, { fileName: '2.html', fileSize: 3000 }, { fileName: '3.txt', fileSize: 5000 } ]); // Flash类型上传文件 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. 定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包,该工厂的 create 方法表示获取一个 div 节点, recover 方法表示回收一个 div 节点:var toolTipFactory = (function(){ var toolTipFactory = (function(){ var toolTipPool = []; // toolTip 对象池 return { create: function(){ if ( toolTipPool.length === 0 ){ // 如果对象池为空 var div = document.createElement( 'div' ); // 创建一个 dom document.body.appendChild( div ); return div; }else{ // 如果对象池里不为空 return toolTipPool.shift(); // 则从对象池中取出一个 dom } }, recover: function( tooltipDom ){ return toolTipPool.push( tooltipDom ); // 对象池回收 dom } } })(); // 2. 创建 2 个小气泡节点,并用一个数组 ary 来记录它们 var ary = []; for ( var i = 0, str; str = [ 'A', 'B' ][ i++ ]; ){ var toolTip = toolTipFactory.create(); toolTip.innerHTML = str; ary.push( toolTip ); }; // 3. 假设地图需要开始重新绘制,在此之前要把这两个节点回收进对象池: for ( var i = 0, toolTip; toolTip = ary[ i++ ]; ){ toolTipFactory.recover( toolTip ); }; // 4. 再创建 6 个小气泡: for ( var i = 0, str; str = [ 'A', 'B', 'C', 'D', 'E', 'F' ][ i++ ]; ){ var toolTip = toolTipFactory.create(); toolTip.innerHTML = str; };

9.6 享元模式小结 享元模式是为解决性能问题而生的模式,在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题;
系列链接
  1. JavaScript 设计模式(上)——基础知识
  2. JavaScript 设计模式(中)——1.单例模式
  3. JavaScript 设计模式(中)——2.策略模式
  4. JavaScript 设计模式(中)——3.代理模式
  5. JavaScript 设计模式(中)——4.迭代器模式
  6. JavaScript 设计模式(中)——5.发布订阅模式
  7. JavaScript 设计模式(中)——6.命令模式
  8. JavaScript 设计模式(中)——7.组合模式
  9. JavaScript 设计模式(中)——8.模板方法模式
  10. JavaScript 设计模式(中)——9.享元模式
  11. JavaScript 设计模式(中)——10.职责链模式
  12. JavaScript 设计模式(中)——11. 中介者模式
  13. JavaScript 设计模式(中)——12. 装饰者模式
  14. JavaScript 设计模式(中)——13.状态模式
  15. JavaScript 设计模式(中)——14.适配器模式
  16. JavaScript 设计模式(下)——设计原则
  17. JavaScript 设计模式练习代码
本文主要参考了《JavaScript设计模式和开发实践》一书

    推荐阅读