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