【学习笔记javascript设计模式与开发实践(享元模式)----12】

第12章 享元模式

享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

如果系统因为创建大量类似的对象而导致内存占用过高,享元模式就非常有用了。在javascript中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。

12.1 初识享元模式

假设有个内衣工厂,目前的产品有50种男式内衣和50种女士内衣,为了推销产品,工厂生产一些塑料模特来穿上他们的内衣拍成广告照片。正常情况下50个男模特和50个女模特,然后让他们每人分别穿一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:

var Model =function(sec,underware){
   this.sex= sex;
   this.underware= underware;
}

Model.prototype.takePhoto=function(){
  console.log('sex='+this.sex+'underwear='+this.underware);
}

for(vari=1;i<=50;i++){
   var maleModel=newModel('male','underwear'+i);
   maleModel.takePhoto();
}

for(vari=1;i<=50;i++){
   var femaleModel=newModel('female','underwear'+i);
    femaleModel.takePhoto();
}

要得到一张照片,每次都需要传入sex和underwear参数,如上所述,现在一共50种男内衣和50种女内衣,所以一共产生100个对象。如果将来生产1000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃了。

下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和50个女模特。其实男女模特各一个就足够了,他们可以分别穿上不同的内衣来拍照。

现在来改写一下代码,既然只需要区别男女模特,那我们先把underwear参数从构造函数中移除,构造函数只接收sex参数

var Model = function(sex){
   this.sex = sex;
}
Model.prototype.takePhoto = function(){
  console.log(‘sex=’+this.sex+’underware=’+this.underwear);
}

分别创建一个男模特对象和女模特对象

var maleModel = new Model(‘male’),femaleModel= new Model(‘male’);
//给男模特拍照
for(var i=1;i<=50;i++){
  maleModel.underware = ‘underware’+i;
  maleModel.takePhoto();
}
//同理女模特
for(var i=1;i<=50;i++){
  femaleModel.underware = ‘underware’+i;
  femaleModel.takePhoto();
}

可以看出,改进之后,只需要两个对象便完成了同样的功能。

12.2 内部状态与外部状态

享元模式要求将对象的属性划分为内部状态与外部状态。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引:

l   内部状态存在于对象内部

l   内部状态可以被一些对象共享

l   内部状态独立于具体的场景,通常不会改变

l   外部状态取决于具体场景,并根据场景而变化,外部状态不能被共享

 

这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并存储丰外部。

剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道。因此,享元模式是一种用时间换空间的优化模式。

在上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象。因为性别有两个,所以最多只需要2个对象。

使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态,如同不管什么样式的衣服,都可以按照性别不同,穿在同一个男模特或女模特身上,模特的性别就可以作为内部状态储存在共享对象的内部。而外部状态取决于具体场景,并根据场景变化,就像例子中每件衣服是不同的,它们不能被一些对象共享,因此只能被划分为外部状态

12.3 享元模式的能用结构

上节中我们初步展示了享元模式的威力,但这清空不是一个完整的享元模式,在这个例子中还存在以下两个问题:

l   我们通过构造函数显式new 出来的男女两个model对象,在其他系统中,也许并不是一开始就需要所有共享对象

l   给model对象手动设置了underwear外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得因难。

我们通过一个对象工厂来解决第一个问题,只有当某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

12.4 文件上传的例子

作者曾在微云上传模块的开发中,我们曾经借助享元模式提升程序的性能。

12.4.1 对象爆炸

在微云上传模块的开发中,作者曾经经历过对象爆炸的问题。微云的文件上传功能虽然可以选择依照队列,一个一个地排队上传,但也支持同时选择2000个文件。每个文件都对应着一个javascript上传对象的创建,在第一版开发中,的确往程序里同时new 了2000个upload对象,结果可想而知,Chrome还勉强能够支撑,IE下直接进入假死状态。

微云支持好几种上传方式,比如浏览器插件、Flash和表单上传等,为了简化例子,我们先假设只有插件和Flash这两种。不论是插件上传,还是Flash上传,原理都一样的,当用户选择了文件之后,插件和Flash都会通知调用window下的全局javascript函数,它的名字是startUpload,用户选择的文件列表被组合成一个数组files塞进该函数的参数列表里,如下:

var id=0;
window.startUpload=function(uploadType,files){
   for(vari=0,file;file= files[i++];){
     var uploadObj=new upload(uploadType,file.fileName,file.fileSize);
     uploadObj.init(id++);
   }
}

当用户选择完文件之后,startUpload函数会遍历files数组来创建对应的upload对象。接下来定义upload构造函数,它接受3个参数,分别是插件类型、文件名和文件大小。这些信息都已经被插件组装在files数组里返回,如下:

Upload.prototype.init=function(id){
  var that=this;
  this.id= id;
  this.dom=document.createEelement('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对象的其他功能,只保留删除文件的功能,对应的方法是upload.prototype.deFile。该方法有一个逻辑,当被删除的文件小于3000KB时,该文件将直接删除。否则页面会弹出一个提示框,提示用户是否确认要删除该文件,代码如下:

Upload.prototype.delFile = function(){
  if(this.fileSize<3000){
   return this.dom.parentNode.removeChild(this.dom);
   }
  if(window.confirm(‘确定要删除该文件吗?’+this.fileName)){
     returnthis.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 享元模式重构文件上传

上节的代码是第一版的文件上传,在这段代码里有多少个需要上传的文件,就一共创建了多少个upload对象,接下来我们用享元模式重构它。

首先我们需要确认插件类型uploadType的内部状态,那为什么单单uploadType是内部状态呢?前面讲过,划分内部状态和外部状态的关键主要有以下几点

o  内部状态存储于对象内部

o  内部状态可以被一些对象共享。

o  内部状态独立于具体场景,通常不会改变

o  外部状态取决于具体场景,并根据场景而变化,外部状态不能被共享

在文件上传的例子里,upload对象必须依赖uploadType属性才能工作,这是因为插件上传、Flash上传、表单上传的实际工作原理有很大区别,它们各自调用的接口也是不一样的、必须在对象创建之初就明确它是什么类型的插件,才可以在程序运行过程中,让它们分别调用各自的start、pause、cancel、del等方法。

实际上在微云的真实代码中,虽然插件和Flash上传对象最终创建自一个大工厂类,但它们实际上根据uploadType值不同,分别是来自于两个不同的对象。(在目前的例子中,为了简化代码,我们把插件和Flash的构造函数合并成了一个。)

一旦明确了uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的。而fileName和fileSize是根据场景而变化的,每个文件的fileName和fileSize都不一样,fileName和fileSize没办法共享,它们只能被划分为外部状态。

12.4.3 剥离外部状态

明确了uploadType作为内部状态之后,我们再把其他的外部状态从构造函数中抽离出来,Upload构造函数中只保留uploadType参数:

var Upload =function(uploadType){
   
this.uploadType=uploadType;
}

Upload.prototype.ini函数也不再需要,因为upload对象初始化的工作被放在uploadManager.add函数里面,接下来只需要定义Upload.prototype.del函数即可:

Upload.prototype.delFile = function(id){

uploadManager.setExternalState(id,this);//(1)

  if(this.fileSize<3000){

   return this.dom.parentNode.removeChild(this.dom);

   }

  if(window.confirm(‘确定要删除该文件吗?’+this.fileName)){

    return this.dom.parentNode.removeChild(this.dom);

   }

}

 

在开始删除文件之前,需要读取文件的实际大小,而文件的实际大小被储存在外部管理器uploadManager中,所以在这里需要通过uploadManager.setExternalState方法给共享对象设置正确的fileSize,上段代码中的(1)处表示把当前id对应的对象的外部状态都组装到共享对象中。

12.4.4 工厂进行对象实例化

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

var UploadFactory= (function(){
   var createdFlyWeigthObjs = {};
   return{
      create:function(uploadType){
         if(createdFlyWeigthObjs[uploadType]){
            returncreatedFlyWeigthObjs[uploadType];
         }
         returncreatedFlyWeigthObjs[uploadType] =newUpload(uploadType);
      }
   }
})();

12.4.5 管理器封装外部状态

现在我们来完美前面提到uploadManager对象,它负责向UploadFactory提交创建对象的请求,并用一个uploadDatabase对象保存所有upload对象的外部状态,以便在程序运行过程中给upload共享对象设置外部状态:

var  uploadManager= (function(){
   var uploadDatabase = {};
   return{
      add:function(id,uploadType,fileName,fileSize){
         varflyWeightObj=UploadFactory.create(uploadType);
         vardom=document.createElement('div');
         dom.innerHTML='<span>文件名:'+fileName+',文件大小:'+fileSize+'</span>'+
                         '<buttonclass="delFile">删除</button>';
          dom.querySelector('.delFile').onclick = function(){
               flyWeightObj.delFile(id);
          }
          document.body.appendChild(dom);
          uploadDatabase[id] = {
             fileName:fileName,
             fileSize:fileSize,
             dom:dom
          };
          returnflyWeightObj;
      },
      setExternalState:function(id,flyWeightObj){
         varuploadData=uploadDatabase[id];
         for(variinuploadData){
           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);
   }
}

享元模式重构的代码之前一共创建6个对象,而通过享元模式重构之后,对象的数量减少为2,更幸运的是,就算现在同时上传2000文件,需要创建的upload对象数量依然是2。

 

12.5 享元模式的适用性

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个factory对象和一个manager对象。

o  一般在以下情况使用享元模式

o  一个程序中使用了大量的相似对象

o  使用大量对象造成很大的内存开销

o  对象的大多数状态都可以变为外部状态

o  剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。

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

我们知道,实现享元模式的关键是把内部状态和外问状态分离开来。有多少种内部状态的组合,系统中中便最多存在多少个共享对象,而外部状态储存在共享对象的外部,在必要时被传入共享对象来组装成一个完整的对象。现在来考虑两个极端的情况,即对象没有外部状态和没有内部状态的时候。

12.6.1 没有内部状态的享元

如果我们的网站很小,并只支持一文件上传模式,这意味着在之前的代码中作为内部状态的uploadType属性是可以删除掉的。

在继续使用享元模式的前提下,构造函数Upload就变成了无参数的形式:

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 没有外部状态的享元

网上许多资料中,经常把java和c#的字符串看成享元,这种说话是否正确呢?

我们来看看java的代码分析:

public class Test{
  public static void main(String args[]){
  String a1 = newString(“a”).intern();
  String a2 = newString(“a”).intern();
 System.out.printlin(a1===a2); //true
   }
}

这段代码,分别new了两个字符串对象a1和a2。intern是一种对象池技术,new String(“a”).intern()的含义如下:

o  如果值为a的字符串对象已经存在于对象池中,则返回这个对象的引用。

o  反之,将字符串a的对象添加进对象池,并返回这个对象的引用

所以结果为true,但这并不是用享元模式的结果,享元模式的关键是区别内部状态和外问状态。享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻再把外部状态组装进共享对象。这里并没有剥离外部状态的过程,a1和a2指向的完全就是同一个对象,所以如果没有外部状态的分离,即使这里使用共享的技术,但并不是一个纯粹的享元模式。

12.7 对象池

我们在前面已经提出了java中String的对象池,下面就业学习这种共享技术。对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取,如果对象池里没有空闲对象,则创建一个新的对象,当获取的对象完成它的职责之后,再进入池子等待被下次获取。

对象池技术应用非常广泛,http连接池和数据库连接池都是其代表应用。在Web前端开发中,对象池使用最多的场景大概就跟DOM有关的操作。很多空间和时间都消耗在了DOM节点上,如何避免频繁地创建和删除DOM节点就成了一个有意义的话题。

12.7.1 对象池的实现

假设我们开发一个地图应用,地图上经常会出现一些标志地名的小气泡,我们叫它toolTip。

先定义一个获取小气泡节点的工厂,作为对象池的数组组成私有属性被包含在工厂闭包里,这个工厂有两个暴露对外的方法,create表示获取一个div节点,receover表示回收一个div节点:

var toolTipFactory = (function(){
    var toolTipPool= [];//toolTip对象池
    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);
       }
    }
})();

创建2个小汽泡,用一个数组ary来记录它们:

var ary = [];
for(vari=0,str;str=['a','b'][i++];){
   var toolTip=toolTipFactory.create();
   toolTip.innerHTML=str;
   ary.push(toolTip);
}

接下来假设需要开始重新绘制,在之前要把这两个节点回进对象池

for(var i=0,toolTip;toolTip = ary[i++]){
  toolTipFactroy.recover(toolTip);
}

再创建6个小气泡

for(var i=0,str;str = [‘a’,’b’,’c’,’d’,’e’,’f’][i++]){
  var toolTip =toolTipFactory.create();
  toolTip.innerHTML= str;
}

再测试一番,页面中出现分别为abcdef6个节点,上次创建好的节点被共享给了下一次操作。对象池跟享元模式的思想有点相似,虽然innerHTML的值也可以看成节点的外部状态,但在这里我们并没有主动分离内部状态和外部状态的过程

12.7.2 通过对象池实现

我们还可以在对象池工厂里,把创建对象的具体过程封装起来,实现一个通用的对象池:

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

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

var iframeFactory =objectPoolFactory(function(){
    var iframe=document.createElement('iframe');
    document.body.appendChild(iframe);
    iframe.οnlοad=function(){
       iframe.οnlοad=null;
       iframeFactory.recover(iframe);
    }
    return 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)


对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外问状态的过程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值