JavaScript设计模式笔记-亨元模式

定义

亨元模式是一种用于性能优化的模式,亨元模式的核心是运用共享技术来有效的支持大量细粒度的对象.

12.1 初始亨元模式

假设有个内衣厂 有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",i) 
   maleModel.takePhoto(); 
} 
for(var j=1;j<=50;j++){ 
   var maleModel=new Model("female",j) 
   maleModel.takePhoto(); 
} 

每次要得到一张照片都需要传递sex和underwear参数.上述的例子new了100个对象…如果数据量太大 就瞬间爆炸了.

下面来分析下.虽然有100种内衣,但是并不需要50个男生和女生,实际上男生和女生就一个 然后让他们穿上不同的衣服就行了 下面来改写下.

var Model=function(sex){ 
   this.sex=sex 
} 
Model=prototype.takePhoto(){ 
   console.log("sex="+this.sex+"underwear="+this.underwear) 
} 
//请一个男生和一个女生 
var male=new Model("male"); 
var female=new Model("female"); 

//给男模特穿上所有的男装 

for(var i=1;i<=50;i++){ 
   male.underwear=i 
   male.takePhoto(); 
} 

//给女模特穿上所有的女装 

for(var j=1;j<=50;j++){ 
   female.underwear=i 
   female.takePhoto(); 
} 

这样只需要两个对象就可以完成需求了

12.2 内部状态与外部状态

亨元模式的目标是尽可能减少共享对象的数量.以下有几条规则

  • 内部状态存储于对象内部.
  • 内部状态可以被一些对象共享.
  • 内部状态独立于具体的场景,通常不会改变
  • 外部状态取决于具体的场景,并且根据场景而变化,外部状态不能被共享

这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享对象,而外部状态可以从对象身上剥离开来,并且存储在外部.在上述例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系统中的对象数量.通常来讲,内部状态有多少种组合,系统中便存在多少个对象,因为性别通常只有男女两种,所以内衣厂商最多只需要两个对象.

使用亨元模式的关键是如何区别内部状态和外部状态,可以被共享的属性通常被划分为内部状态,而外部状态则取决于具体的场景,并且根据场景而变化.

12.3 亨元模式的通用结构

12.1节还存在两个问题

  • 我们通过构造函数显示的new出了男女两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象(这里可以通过工厂模式解决)
  • 给model对象手动设置了underwear外部状态,在更复杂的系统中,这并不是一个好方法,因为外部状态可能会相当复杂,它们与共享对象的联系会变得更加困难(这里可以用一个管理器来记录对象相关的外部状态,使外部状态通过某个钩子与共享对象联系起来)

12.4 文件上传的例子

12.4.1 对象爆炸

作者在开发微云的时候遇到过对象爆炸问题 一次性new了2000个对象…具体看代码

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函数会遍历files数组来创建对应的upload对象,接下来定义upload构造函数.3个参数分别为 插件类型 文件名 文件大小 这些信息都已经被插件组装在files数组里返回

var Upload=function(uploadType,fileName,fileSize){ 
   this.uploadType=uploadType; 
   this.fileNmae=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 ); 
} 

再写删除函数 当文件大小小于3000KB则直接删除 否则会弹出一个提示框确认

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

接下来创建3个插件上传和3个flash上传对象

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

12.4.2 亨元模式重构文件上传

上一节有多少个上传文件就有多少个对象,那么我们用亨元模式重构 首先确定内部和外部状态.内部状态为uploadType.upload对象必须依赖uploadType属性才能工作.一但明确了uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的.而fileName和fileSize是根据场景而变化的.每个fileName和fileSize都不一样.无法被共享

12.4.3 剥离外部状态

明确了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);//给共享对象设置正确的fileSize 把外部状态组装到共享对象中 
   if ( this.fileSize < 3000 ){ 
       return this.dom.parentNode.removeChild( this.dom ); 
   } 

   if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){ 
       return this.dom.parentNode.removeChild( this.dom ); 
   } 
} 

12.4.4 工厂进行对象实例化

接下来定义一个工厂来创建upload对象.如果内部某种状态对应的共享对象已经被创建过 那么返回这个对象 否则返回一个新对象

var UploadFactory=(function(){ 
   var createFlyWeightObjs={}; 
   return{ 
       create:function(uploadType){ 
           if(createFlyWeightObjs[uploadType]){ 
               return createFlyWeightObjs[uploadType]; 
           } 
           return createFlyWeightObjs[uploadType]=new Upload(uploadType); 
       } 
   } 
})() 

12.4.5 管理器封装外部状态

下面来完善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 = 
               '<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 uploadDate=uploadDatebase[id]; 
           for(var i in uploadData){//把fileName,fileSize,dom传递给flyWeightObj 
               flyWeightObj[i]=uploadDate[i] 
           } 
       } 
   } 
})() 

这里点击删除按钮会触发 flyWeightObj的delFile函数,并且传递了一个id.那么在delFile函数中会调用一个uploadManager.setExternalState方法 传递了两个参数一个是id 一个是this那么这里的this指向的是当前的执行环境也就是当前的Upload.然后setExternalState会根据id在uploadDatebase把当前存储的信息找到 赋值给Upload对象 这样就完成了外部状态传递进共享对象中

然后触发上传动作的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 
   } 
   ]); 
   startUpload( 'flash', [ 
   { 
       fileName: '4.txt', 
       fileSize: 1000 
   }, 
   { 
       fileName: '5.html', 
       fileSize: 3000 
   }, 
   { 
       fileName: '6.txt', 

       fileSize: 5000 
   } 
   ]); 

重构后的对象只有两个.

12.5 亨元模式的适用性

亨元模式是一种很好的性能优化方案,使用了亨元模式之后,我们需要多维护一个factory和一个manager对象.亨元模式带来的好处很大程度上取决于如何使用以及何时使用 一般来说.

  • 一个程序中使用了大量的相似对象.
  • 由于使用了大量对象,造成很大的内存开销
  • 对象的大多数状态都可以变为外部状态
  • 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象

12.6 再谈内部状态和外部状态

实现亨元模式的关键是把内部状态和外部状态分离开来,现在考虑两种极端的情况 即对象没有内部和外部状态.

12.6.1 没有内部状态的亨元

很多网站其实就只有一种上传方式,那么上述的upload中uploadType就可以删掉了

var Upload=function(){} 

其他属性 fileName,fileSize,dom依然可以作为外部状态保存在共享对象内部,那么这意味着我们需要改写创建亨元对象的工厂

var UploadFactory=(function(){ 
   var uploadObj; 
   return{ 
       create:function(){ 
           if(uploadObj){ 
               return uploadObj 
           } 
           return uploadObj=new Upload(); 
       } 
   }     
}) 

管理器代码不动,还是负责剥离和组装外部状元.可以看到,当对象没有内部状态的时候,生成对象的工厂实际上变成了一个单例工厂.

12.6.2 没有外部的亨元——参考对象池

12.7 对象池

把创建过的对象保存起来 当需要的时候再把对象拿出来 保存多个对象和拿出多个就叫池 比如图书馆. 书看完了可以放回去,下次有大量需要的时候那么可以从图书馆拿

12.7.1 对象池实现

地图应用的toolTip.比如第一个地图出现了两个toolTip 当刷新地图的时候出现了6个tooltip 当然我们不可能把原来存在的tooltip删除然后重新创建6个 而是回收原来的两个再创建4个

var toolFactory=(function(){ 
   var toolTipPool=[]; 
   return { 
       create:function(){ 
           if(toolTipPool.length==0){//如果对象池为空 
               var div=document.createElement("div")//创建一个dom 
               document.body.appendChild(div); 
               return div 
           }else{ 
               return toolTipPool.shift() 
           } 
       }, 
       recover:function(tooltipDom){ 
           return toolTipPool.push(tooltipDom)//对象池回收dom 
       } 
   } 
})() 

比如需要创建两个小气泡节点.为了方便回收利用 那么可以用一个ary来记录它们

var ary=[]; 
for(var i=0,str;str=["A","B"][i++]){ 
   var toolTip=toolFactory.create(); 
   toolTip.innerHTML=str; 
   ary.push(toolTip); 
} 

假设地图要重新开始绘制,那么先把这两个节点回收进对象池

for(var i=0,toolTip;toolTip=ary[i++];){ 
   toolTipFactory.recover(toolTip);//回收对象的时候会把innerHTML都回收 如果下次使用不指定innerHTML那么就会返回上次设置的innerHTML 
} 

再创建6个小气泡

for(var i=0,str;str=['S','Z'][i++];){ 
       var toolTip=toolTipFactory.create() 
       toolTip.innerHTML=str; 
} 

12.7.2 通用对象池的实现

var objectPoolFactory=function(createObjFn){ 
   var objectPoll=[]; 
   return{ 
       create:function(){ 
           var obj=objectPoll.length==0?createObjFn.apply(this,arguments):objectPoll.shift(); 
           return obj 
       }, 
       recover:function(obj){ 
           objectPoll.push(obj) 
       } 
   } 
} 

现在利用objectPollFactory来创建一个装载一些iframe的对象池

var iframeFactory=objectPoolFactory(function(){ 
       var iframe=document.createElement("iframe"); 
       document.body.appendChild(iframe); 
       iframe.onload=function(){ 
           iframe.onload=null;//防止重复加载 
           iframeFactory.recover(iframe);//加载完毕后回收节点 
       } 
   }); 
   var iframe1 = iframeFactory.create(); 
   iframe1.src = 'http:// baidu.com'; 
   var iframe2 = iframeFactory.create(); 
   iframe2.src = 'http:// QQ.com'; 
   setTimeout(function(){ 
       var iframe3 = iframeFactory.create(); 
       iframe3.src = 'http:// 163.com'; 
   }, 3000 );