享元(flyweight)模式。它最适合于解决因创建大量类似对象而累及性能的问题。这种模式在javascript中尤其有用,因为复杂的javascript代码可能很快就会用光浏览的甩有可用内存。通过把大量独立的对象转化为少量其享对象,可以降低运行Web应用程序所需要 的资源数量。这种技术带来的好处可大可小。对于那些可能一连用上几天也不会重新加载的大型应用系统,任何减少内存用量的技术都有非常显著的效果。而对于那些不会在浏览器中打开那么长时间的小型网页,内存的节约就没那么重要。
刚接触享元的模式的时候其工作机制可能很难理解。我们先对其结构作一概览,然后再详细讲解各个部分。
享元模式用于减少应用程序所需要的对象的数量。这是通过将对象的内部状态划分为内存数据和外在数据两类而实现的。内在数据是指类的内部方法所需要的信息,没有这种数据的话类就不能正常运转。外在数据则是可以从类身上剥离并存储在其外部的信息。我们可以将内存状态相同的所有对象替换为同一个共享对象,用这种方法可以把对象数据减少到不同内存状态的数量。
创建这种共享对象需要使用工厂,而不是普通的构造函数。这样做可以跟踪到已经实例化的各个对象,从而仅当所需对象的内存状态不同于已有对象时才创建一个新对象。对象的外在状态被保存在一个管理器对象中。在调用对象的方法时,管理器会把这些外在状态作为参数传入。
假设要开发一个系统,用以代表一个城市的所有汽车。你需要保存每一辆汽车的详细情况及其所有权的详细情况。当然,你决定把每辆汽车表示为一个对象:
var Car =function(make,model,year,owner,tag,renewDate){
this.make = make;
this.model = model;
this.year = year;
this.owner = owner;
this.tag = tag;
this.renewDate = renewDate;
}
Car.prototype = {
getMake = function(){
return this.make;
},
getModel = function(){
return this.model;
},
getYear = function(){
return this.year;
},
transferOwership:function(newOwner,newTag,newRenewDate){
this.owner = newOwner;
this.tag = newTag;
this.renewDate = newRenewDate;
},
renewRegistration:function(newRenewDate){
this.renewDate = newRenewDate;
},
isRegistrationCurrent:function(){
var today = new Date();
returntoday.getTime() } } 这个系统最初表现不错。但是随着城市人口的增长,你发现它一天天地变慢了。数以十万计的汽车对象耗尽了可用的计算资源。 将对象数据划分为内在和外在部分的过程有一定的随意性。既要维持每个对象的模块性,又想把尽可能多的数据作为外在数据处理。划分依据的选择多少有些主观性。在本例中,车的自然数据(品牌、型号和出厂日期)属于内在数据,而所有权数据(车主姓名、车牌号和最近登记日期)则属于外在数据。这意味着对于品牌、型号和出厂日期的每一种组合,只需要一个汽车对象就行。这个数目还是不少,不过与之前相比已经少了几个数量级。每个品牌型号出厂日期组合对应的那个实例将被所有该类汽车的车主共享。下面是新版Car类: var Car = function(make,model,year){ this.make = make; this.model= model; this.year= year; } Car.prototype = { getMake:function(){ return this.make; }, getModel:function(){ return this.model; }, getYear:function(){ return this.year; } }; 这个工厂很简单。它会检查之前是否已经创建过对应用于指定品牌-型号-出厂日期组合的汽车,如果存在这样的汽车那就返回它,否则就创建一辆新,并把它保存起来供以后使用。这就确保对应于每个唯一的内在状态,只会创建一个一实例: //CarFactorysingleton var CarFactory =(function(){ var createdCars = {}; return { createCar:function(make,model,year){ //check to see if this particular combination has been created before. return createdCars[make+’-‘+model+’-‘+year]; }else{ varcar=new Car(make,model,year); createdCars[make+’-‘+model+’-‘+year]=car; return car; } })(); 要完成这种优化还需要一个对象。所有那些从Car对象中删除的数据必须有个保存地点,我们用一个单体来做封装这些数据的管理器。原先的每一个Car对象现在都被分割为外在数据及其所属的共享汽车对象的引用这样两部分。Car对象与车主数据的组合称为汽车记录。管理器存储着这两方面的信息。它还包含着从原先的Car类删除的方法: /*CarRecordManagersingleton.*/ varCarRecordManager = (function(){ var carRecordDatabase = {}; return { //Add a new car record into thecity’s system. addCarRecord:function(make,model,year,owner,tag,renewDate){ var car =CarFactory.createCar(make,model,year); carRecordDatabase[tag] = { owner:owner, renewDate:renewDate, car:car } }, //Methods previously contained inthe Car class transferOwnership:function(tag,newOwner,newTag,newRenewDate){ var record = carRecordDatabase[tag]; record.owner = newOwner; record.tag = newTag; record.renewDate =newRenewDate; }, renewRegistration:function(tag,newRenewDate){ carRecordDatabase[tag].renewDate= newRenewDate; }, isRegistrationCurrent:function(tag){ var today = new Date(); returntoday.getTime } } })(); 从Car类剥离的所有数据现在都保存在CarRecordManager这个单体的私用属性carRecordDatabase中。这个carRecordDatabase对象要比以前使用一大批对象高效得多。那些处理所有权事宜的方法现在也被封装在这个单体中,因为它处理的都是外在数据。 可能看出,这种优化是以复杂性为代价的。原先有的只是一个类,而现在却变成了一个类和两个单体对象。把一个对象的数据保存在两个不同的地方这种做法也有点令人困惑。但与所解决的性能问题相比。这两点都只是小问题。如果运用得当,那么享元模式能够显著地提升程序性能。 管理享元对象的外在数据有许多不同的方法。使管理器对象是一种常见做法,这种对象有一个集中管理的数据库,用于存放外在状态及其所属的享元对象。汽车登记那个示例就采用了这种方法。其优点在于简单、很容易维护。这也是一种比较轻便的方案,因为用来保存外在数据只是一个数组或对象字面量。我们在后面那个工具提示示例中还要使用这种方案 另一种管理外在状态的办法是使用组合模式。你可以用对象自身的层次体系来保存信息,而不需要另外使用一个集中管理的数据库。组合对象的叶节点全都可以是享元对象,这样一来这些享元对象就可以在组合对象层次体系中的多个地方被共享。对于大型的对象层次体系这非常有用,因为同样的数据用这种方案来表示时所需对象的数量要少得多。 首先实现的是一个未经优化、未使用享元的版本。这是一个大型组合对象,位于最顶层的代表年份的组合对象。它封装着代表月分的组合对象,而后者又封装了代表日期的叶对象。 /*CalendarIteminterface*/ var CalendarItem =new Interface(‘CalendarItem’,[‘display’]); /*CalendarYearclass,a composite.*/ var CalendarYear =function(year,parent){ this.year = year; this.element = document.createElement(‘div’); this.element.style.display = “none”; parent.appendChild(this.element); function isLeapYear(){ return (y>0) && !(y%4) && ((y%100)|| !(y%400)); } this.months = []; this.numDays =[31,isLeapYear(this.year)?29:28,31,30,31,30,31,31,30,31,30,31]; for(var i=0,len=12,i this.months[i] = new CalendarMonth(I,this.numDays[i],this.element); } } CalendarYear.prototype= { display:function(){ for(vari=0,len=this.months.length;i this.months[i].display(); } this.element.style.display = ‘block’; } } /*CalendarMonthclass , a composite. */ var CalendarMonth= function(monthNum,numDasy,parent){ this.monthNum = monthNum; this.element = document.createElement(‘div’); this.element.style.display = ‘none’; parent.appendChild(this.element); this.dasy = []; for(var i=0,len=this.days.length;i this.days[i] = new CalendarDay(I,this.element); } } CalendarMonth.prototype= { display:function(){ for(vari=0,len=this.days.length;i this.days[i].display(); } this.element.style.display = ‘block’; } } var CalendarDay =function(date,parent){ this.date = date; this.element = document.createElement(‘div’); this.element.sytle.display = ‘none’; parent.appendChild(this.element); } CalendarDay.prototype= { display:function(){ this.element.style.display = ‘block’; this.element.innerHTML =this.date; } } 这段代码的问题在于,你不得不为每一年创建365个CalendarDay对象。要创建一个显示10年的日历,需要实例化几千个CalendarDay对象。这些对象因然不大,但无论什么类型的对象,如果其数目如此之多的话,都会带来资源压力。更有效的做法无论日历要显示多少年,都只用一个CalendarDay对象来代表所有日期。 把CalendarDay对象转化为享元对象的过程很简单。首先,修改CalendarDay类本身,除去其中保存的的所有数据,让这些数据成为外在数据: /*CalendarDayclass , a flyweight leaf*/ var CalendarDay =function(){}; CalendarDay.prototype= { display:function(date,parent){ var element = document.createElement(‘div’); parent.appendChild(element); element.innerHTML = date; } } 接下来,创建日期对象的单个实例。所有CalendarMonth对象中都要使用这个实例。这里本来也可以像每一个示例那样使用工厂来创建该类的实例,不过,因为这个类只需要创建一个实例,所以直接实例化它就行: var calendarDay =new CalendarDay(); 现在外在数据成了display方法的参数,而不是类的构造函数的参数。这是享元的典型工作方式。因为这些情况下有些数据被保存在对象之外,要想实现与之前同样的功能就必须把它们提供给各个方法。 最后,CalendarMonth类也要略作修改。原来用CalendaryDay类构造创建该类实例的那个表达式被替换为calendarDay对象,而那些原本提供给CalendarDay类构造函数的参数现在被转而提供给display方法: /*CalendarMonthclass, a composite.*/ var CalendarMonth= function(monthNum,numDays,parent){ this.monthNum = monthNum; this.element = document.createElement(‘div’); this.element.style.display = ‘none’; parent.appendChild(this.element); this.days = []; for(var i=0,len=numDays;i this.days[i]= calendarDay; } } CalendarMonth.prototype= { display:function(){ for(vari=0,len=this.days.length;i this.days[i].display(); } this.element.style.display = ‘block’; } } 先看看未使用享元模式的Tooltip类 var Tooltip =function(targetElement,text){ this.target = targetElement; this.text = text; this.delayTimeout = null; this.delay = 1500; //in milliseconds this.element = document.createElement(‘div’); this.element.sytle.display = ‘node’; this.element.style.position = ‘absolute’; this.element.className=’tooltip’; document.getElementByTagName(‘body’)[0].appendChild(this.elemnt); this.element.innerHTML = this.text; //attach the events var that = this; addEvent(this.target,’mouseover’,function(e){that.startDelay(e);}); addEvent(this.target,’mouseout,function(e){that.hide(e);}) } Tooltip.prototype= { startDelay:function(e){ if(this.delayTimeout ==null){ var that = this; var x = e.clientX; var y = e.clientY; this.delayTimeout =setTimeout(function(){ that.show(x,y); },this.delay); } }, show:function(){ clearTimeout(this.delayTimeout); this.delayTimeout=null; this.element.style.left = x+”px”; this.element.style.top = (y+20)+”px”; this.element.style.display = ‘block’; }, hide:function(){ clearTimeout(this.delayTimeout); this.delayTimeout = null; this.element.style.display = ‘none’; } } 把Tooltip类转化为享元需要做三件事: 1. 把外在数据从tooltip对象中删除; 2. 创建一个用来实例化Tooltip的工厂; 3. 创建一个用来保存外在数据的管理器。 var Tooltip =function(){ this.delayTimeout = null; this.delay = 1500; //create the html this.element = document.createElement(‘div’); this.element.sytle.display = “none”; this.element.style.position = “absolute”; this.element.className = “tooltip”; document.getElementsByTagName(“body”)[0].appendChild(this.element); } Tooltip.prototype= { startDelay:function(e,text){ if(this.dealyTimeout ==null){ var that = this; var x = e.clientX; var y = e.clientY; this.delayTimeout =setTimeout(function(){that.show(x,y,text);},this.delay); } }, show:function(x,y,text){ clearTimeout(this.delayTimeout); this.delayTimeout = null; this.element.innerHTML = text; this.element.style.left = x+”px”; this.element.style.top = (y+20)+”px”; this.element.style.display = “block”; }, hide:function(){ clearTimeout(this.delayTimeout); this.delayTimeout = null; this.element.style.display = ‘none’; } } 上面的Tooltip类删除了原来的构造函数的所有参数以及注册事件处理代码。而startDelay和show方法则各增加一个新的参数,这一来,要显示的文字就可以作为外在数据传给它们。 下一步是创建用作工厂和管理器的那个单体。我们把Tooltip类的声明放在TooltipManager这个单体中,这样它就不能在别的地方被实例化: /*TooltipManagersingleton , a flyweight factory and manager*/ var TooltipManager= (function(){ var storedInstance = null; /*Tooltip class, as a flyweight.*/ var Tooltip = function(){ //… } Tooltip.prototype = { //… } return{ addTooltip:function(targetElement,text){ var tt = this.getTooltip(); //attach the events; addEnvet(targetElement,’mouseover’,function(e){tt.startDelay(e,text);}); addEnvet(targetElement,’mouseout,function(e){tt.hide();}); }, getTooltip:function(){ if(storedInstance==null){ storedInstance = newTooltip(); } return storedInstance; } } })(); 1. 外在数据从目标类剥离 2. 创建一个用来控制该类的实例化的工厂 3. 创建一个用来保存外在数据的管理器内在状态和外在状态
用工厂进行实例化
封装在管理器中的外在状态
管理外在状态
示例:web日历
把日期对象转化享元
示例:工具提示对象
未经优化的Tooltip类
作为享元的Tooltip
创建步骤