本章探讨另一种优化模式-享元模式,它最适合于解决因创建大量类似对象而累及性能的问题。这种模式在Javascript中尤其有用,因为复杂的javascript代码很快就会用光浏览器的所有可用内存,通过把大量独立对象转化为少量共享对象,可以降低运行web应用程序所需的资源数量。
享元模式用于减少应用程序所需对象的数量。这是通过对对象的内部状态划分为内在数据和外在数据俩类实现的。内在数据是指类的内部方法所需要的信息,没有这种数据的话类就不能正常运转。外在数据则是可以从类身上剥离并存储在其外部的信息。我们可以将内在状态相同的所有对象替换为同一个共享对象,用这种方法可以把对象数量减少到不同内在状态的数量。
创建这种共享对象需要使用工厂,而不是构造函数,这样就可以跟踪到已经实例化的各个对象,从而仅当所需对象的内在状态不同于已有对象时才创建一个新对象,对象的外在状态被保存在一个管理器中,在调用对象的方法时,管理器会把这些外在状态作为参数传入。
汽车登记示例:
假设你要开发一个系统,用以代表一个城市的所有汽车,你需要保存每一辆汽车的详细情况(品牌、型号和出厂日期)及其所有权的详细情况(车主,车牌、登记日期)。当然你决定把每辆汽车表示为一个对象。
var Car = function (make,model,year,owner,tag,renewDate) { this.make = make; this.model = model; this.yuea = 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; }, transferOwnership: 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(); return today.getTime() < Date.parse(this.renewDate); }};
这个系统开始表现不错,但是随着城市人口的增长,数以十万计的汽车对象耗尽了可用的计算资源,想要优化这个系统,可以采用享元模式减少所需对象的数目。而优化工作的第一步,就是把内在状态和外在状态分开。
将对象划分为内在和外在的过程有一定的随意性。既要维持每个对象的模块性,又要尽可能多的数据做外在数据处理。在本例中,车的自然属性(品牌,型号,出厂如期)属于内在数据,而所有权数据(车主、车牌、登记日)属于外在数据,这意味着对于品牌、型号、和出厂日期的每一种组合,只需要一个汽车对象就成。每个品牌、型号、出厂日期组合对应的那个实例将被所有该类汽车的车主共享。下面是新版代码.
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; }};
上面的代码删除了所有外在数据。所有的处理登记事宜方法都被转移到一个管理器对象中(不过也可以将这些方法留在原地,并为其增加对应于各种外在数据的参数)。因为现在对象的数据已经被分为俩大部分,所以必须用工厂来实例化它。
用工厂进行实例化
这个工厂很简单,它会检查之前是否已经创建过对应于指定品牌、型号、出厂日期组合的汽车,如果存在这样的汽车那就返回,否则就创建一辆新车,并把它保存起来以便以后使用。这就确保了对应于每个唯一的内在状态,只会创建一个实例:
var CarFactory = (function(){ var createdCars = {}; return { createCar:function(make,model,year) { if(createdCars[make+'-'+model+'-'+year]) { return createdCars[make+'-'+model+'-'+year]; }else{ var car = new Car(make, model, year); createdCars[make+'-'+model+'-'+year] = car; return car; } } }})();
封装在管理器中的外在状态:
要完成这种优化还需要一个对象。所有那些从Car对象中删除的数据必须有个保存的地点,我们用一个单体来做封装这些数据的管理器。原先的每一个Car对象现在都被分割为外在数据及其所属的共享汽车对象的引用这样俩部分。Car对象与车主数据的组合称为汽车记录,管理器存储着这俩方面的信息,它还包含着从原先Car类删除的方法:
var CarRecordManager = (function(){ var carRecordDatabase = {}; return { addCarRecord:function(make,model,year,owner,tag,renewDate) { var car = CarFactory.createCar(make, model, year); carRecordDatabase[tag] = { owner:owner, renewDate:renewDate, car:car }; }, transferOwnership: function (tag,newOwner, newTag, newRenewDate) { var record = carRecordDatabase[tag]; record.owner = newOwner; record.tag = newTag; record.renewDate = newRenewDate; }, renewRegistration: function (newRenewDate) { carRecordDatabase[tag].renewDate = newRenewDate; }, isRegistrationCurrent: function () { var today = new Date(); return today.getTime() < Date.parse(carRecordDatabase[tag].renewDate); } }})();
从Car类剥离的所有数据都保存在 CarRecordManager这个单体的私有属性 carRecordDatabase中,这个carRecordDatabase对象要比以前使用的一大批对象高效得多。那些处理所有权的事宜的方法现在也被封装在这个单体中,因为他们处理的都是外在数据。
可见,这种优化是以复杂性为代价的,原先只有一个类,现在变成了一个类和俩个单体对象,但与所解决的性能问题相比,这都是小问题。
管理外在状态
管理享元对象的外在数据有许多不同的方法,使用管理器对象是一种常见的做法。这种对象有一个集中管理的数据库,用于存放外在状态及其所属的享元对象。汽车登记那个示例就采用了这种方案。
另一种管理外在状态的办法是使用组合模式,借助前面的组合模式,你可以用对象自身的层次体系来保存信息,而不需要另外使用一个集中管理的数据库。组合对象的叶节点全都可以是享元对象,这样一来这些享元对象就可以在组合对象层次体系中的多个地方被共享。对于大型的对象层次体系这非常有用,因为同样的数据用这种方案来表示时所需对象的数量要少的多。
WEB 日历示例:
为了演示组合对象来保存外在状态的具体做法,我们创建一个web日历,首先实现的是一个未经优化,未使用享元的版本。这是一个大型组合对象,位于最顶层的代表年份的组合对象,它封装着代表月份的组合对象,而后者又封装着代表日期的叶对象。这是一个简单的例子,他会按顺序显示每月的各天,还会按顺序显示一年中个各个月:
var CalendarItem = new Interface('CalendarItem', ['display']);var CalendarYear = function(year,parent) { this.year = year; this.element = document.createElement('div'); this.element.style.display = 'none'; parent.appendChild(this.element); function isLeapYear(y) { 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<len;i++) { this.months[i] = new CalendarMonth(i,this.numDays[i],this.element); }};CalendarYear.prototype = { display:function(){ for (var i= 0,len=this.months.length;i<len;i++) { this.months[i].display(); } this.element.style.display = 'block'; }}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<len;i++) { this.days[i] = new CalendarDay(i,this.element); }};CalendarMonth.prototype = { display:function(){ for (var i= 0,len=this.days.length;i<len;i++) { this.days[i].display(); } this.element.style.display = 'block'; }}var CalendarDay = function (date,parent) { this.date = monthNum; this.element = document.createElement('div'); this.element.style.display = 'none'; parent.appendChild(this.element);};CalendarDay.prototype = { display:function(){ this.element.style.display = 'block'; this.element.innerHTML = this.date; }}
这段代码的问题在于你不得不为每一年创建365个 CalendarDay,如果创建一个十年日历,会给浏览器带来资源压力,更有效的做法是无论日历要显示多少年,都只用一个CalendarDay对象来代表所有日期。
把日期对象转化为享元模式实现:
把CalendarDay对象转化为享元对象的过程很简单。首先,修改CalendarDay类本身,除去其中保存的所有数据。让这些数据(日期和父元素)成为外在数据:
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类也要略作修改。原来用CalendarDay类构造函数创建该类实例的那个表达式被替换为CalendarDay对象,而那些原本提供给CalendarDay类构造函数的参数现在被转而提供给display方法:
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<len;i++) { this.days[i] = CalendarDay; }};CalendarMonth.prototype = { display:function(){ for (var i= 0,len=this.days.length;i<len;i++) { this.days[i].display(i,this.element); } this.element.style.display = 'block'; }}
本例没有像前面那样使用一个中心数据库来保存所有从享元对象剥离的数据。实际上,其他类基本没什么修改。CalendarYear根本没有改变,CalendarMonth只修改了俩行,这都是因为组合对象的结构本身就已经包含了所有的外在数据。由于月份对象中是所有日期对象依次存放在一个数组中,所以他们知道每一个日期对象的状态、从CalendarDay构造函数中提出的俩种数据都已经存在于CalendarMonth对象中。
这就是组合模式与享元模式配合的如此完美的原因,组合对象通常拥有大量的叶对象,它还保存着许多可作为外在数据处理的数据。叶对象通常只包含极少的内在数据,所以很容易被转化为共享资源。
工具提示对象示例:
在javascript对象需要创建html内容这种情况下,享元模式特别有用。那种会生成DOM元素的对象如果数目众多的话,会占用过多内存,采用享元模式后,只需要创建少许这种对象即可,所有需要这种对象的地方都可以共享他们。工具提示就是一个简单例子。
先看看未使用享元模式的Tooltip类。
var Tooltip = function (targetElement, text) { this.target
新闻热点
疑难解答