JavaScript|JavaScript 设计模式(中)——9.享元模式
9 享元模式
享元( flyweight)模式是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象;
9.1 享元模式简单示例
假设目前加工好了50件男士外套和50件女士外套,需要使用塑料模特拍照,正常情况下需要 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', 'underwear' + i );
maleModel.takePhoto();
};
for ( var j = 1;
j <= 50;
j++ ){
var femaleModel= new Model( 'female', 'underwear' + j );
femaleModel.takePhoto();
};
考虑一下如何优化这个场景,其实男模特和女模特各自有一个就足够,代码调整如下:
var Model = function( sex ){ this.sex = sex;
};
Model.prototype.takePhoto = function(){
console.log( 'sex= ' + this.sex + ' underwear=' + this.underwear);
};
var maleModel = new Model( 'male' ), femaleModel = new Model( 'female' );
for ( var i = 1;
i <= 50;
i++ ){
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
};
for ( var j = 1;
j <= 50;
j++ ){
femaleModel.underwear = 'underwear' + j;
femaleModel.takePhoto();
};
9.2 内部状态与外部状态 享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性),享元模式的目标是尽量减少共享对象的数量;
【JavaScript|JavaScript 设计模式(中)——9.享元模式】1.如何划分内部状态和外部状态:
- 内部状态存储于对象内部;
- 内部状态可以被一些对象共享;
- 内部状态独立于具体的场景,通常不会改变;
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享;
在上面的例子中,性别是内部状态,外套是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女两种,所以最多只需要 2 个对象;
- 分析上面的例子:
在上面的例子中,存在的一些问题以及解决方法:
- 通过构造函数显式
new
出了男女两个model
对象,在其他系统中,并不是一开始就需要所有的共享对象;因此通过一个对象工厂来解决,只有当某种共享对象被真正需要时,它才从工厂中被创建出来; - 给
model
对象手动设置了underwear
外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难;因此用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来;
- 文件上传基本实现代码:
//定义 Upload 构造函数,它接受 3 个参数,分别是插件类型、文件名和文件大小
var Upload = function( uploadType, fileName, fileSize ){
this.uploadType = uploadType;
this.fileName = fileName;
this.fileSize = fileSize;
this.dom= null;
};
//upload 对象init函数
Upload.prototype.init = function( id ){
var that = this;
this.id = id;
this.dom = document.createElement( 'div' );
this.dom.innerHTML =
'文件名称:'+ this.fileName +', 文件大小: '+ this.fileSize +'' +
'';
this.dom.querySelector( '.delFile' ).onclick = function(){ that.delFile();
}
document.body.appendChild( this.dom );
};
// upload 对象删除文件的功能
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 );
}
};
// 当选择了文件并确认上传后,调用 Window 下的一个全局函数 startUpload,用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里,代码如下:
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( 'plugin', [
{ fileName: '1.txt', fileSize: 1000 },
{ fileName: '2.html', fileSize: 3000 },
{ fileName: '3.txt', fileSize: 5000 }
]);
// Flash类型上传文件
startUpload( 'flash', [
{ fileName: '4.txt', fileSize: 1000 },
{ fileName: '5.html', fileSize: 3000 },
{ fileName: '6.txt', fileSize: 5000 }
]);
该方式的文件上传中,若一次性上传很多个文件时,每一个文件对应一个上传对象,这种对象爆炸的问题会使得浏览器崩溃;
在文件上传的例子里, upload 对象必须依赖
- 享元模式重构文件上传:
uploadType
属性才能工作,这是因为插件上传、Flash 上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,因此 uploadType
作为内部状态,把其他的外部状态从构造函数中抽离出来,Upload 构造函数中只保留 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 );
// 表示把当前 id 对应的对象的外部状态都组装到共享对象中
if ( this.fileSize < 3000 ){
return this.dom.parentNode.removeChild( this.dom );
}
if ( window.confirm( '确定要删除该文件吗? ' + this.fileName ) ){
return this.dom.parentNode.removeChild( this.dom );
}
工厂进行对象实例化:定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:
var UploadFactory = (function(){
var createdFlyWeightObjs = {};
return {
create: function( uploadType){
if ( createdFlyWeightObjs [ uploadType] ){
return createdFlyWeightObjs [ uploadType];
}
return createdFlyWeightObjs [ uploadType] = new Upload( uploadType);
}
}
})();
管理器封装外部状态:
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 =
'文件名称:'+ fileName +', 文件大小: '+ fileSize +'' +
'';
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 uploadData = https://www.it610.com/article/uploadDatabase[ id ];
for ( var i in uploadData ){ 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 );
}
};
最后测试,运行下面的代码后,可以发现运行结果跟用享元模式重构之前一致:
// 插件类型上传文件
startUpload( 'plugin', [
{ fileName: '1.txt', fileSize: 1000 },
{ fileName: '2.html', fileSize: 3000 },
{ fileName: '3.txt', fileSize: 5000 }
]);
// Flash类型上传文件
startUpload( 'flash', [
{ fileName: '4.txt', fileSize: 1000 },
{ fileName: '5.html', fileSize: 3000 },
{ fileName: '6.txt', fileSize: 5000 }
]);
9.4 享元模式的适用性 享元模式的适用场景:
- 一个程序中使用了大量的相似对象;
- 由于使用了大量对象,造成很大的内存开销;
- 对象的大多数状态都可以变为外部状态;
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象;
没有内部状态的享元:
没有外部状态的享元:
2.9.6 对象池 对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接
new
,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取。对象池实现:
假设在一个地图应用中, 地图上经常会出现一些标志地名的小气泡,当搜索附近地图的时候,页面里出现了 2 个小气泡。当我再搜索附近的其他地点时,页面中出现了 6 个小气泡。按照对象池的思想,在第二次搜索开始之前,并不会把第一次创建的2 个小气泡删除掉,而是把它们放进对象池;这样在第二次的搜索结果页面里,只需要再创建 4 个小气泡而不是 6 个;
// 1. 定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包,该工厂的 create 方法表示获取一个 div 节点, recover 方法表示回收一个 div 节点:var toolTipFactory = (function(){
var toolTipFactory = (function(){
var toolTipPool = [];
// toolTip 对象池
return {
create: function(){
if ( toolTipPool.length === 0 ){ // 如果对象池为空
var div = document.createElement( 'div' );
// 创建一个 dom
document.body.appendChild( div );
return div;
}else{ // 如果对象池里不为空
return toolTipPool.shift();
// 则从对象池中取出一个 dom
}
},
recover: function( tooltipDom ){
return toolTipPool.push( tooltipDom );
// 对象池回收 dom
}
}
})();
// 2. 创建 2 个小气泡节点,并用一个数组 ary 来记录它们
var ary = [];
for ( var i = 0, str;
str = [ 'A', 'B' ][ i++ ];
){
var toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
ary.push( toolTip );
};
// 3. 假设地图需要开始重新绘制,在此之前要把这两个节点回收进对象池:
for ( var i = 0, toolTip;
toolTip = ary[ i++ ];
){
toolTipFactory.recover( toolTip );
};
// 4. 再创建 6 个小气泡:
for ( var i = 0, str;
str = [ 'A', 'B', 'C', 'D', 'E', 'F' ][ i++ ];
){
var toolTip = toolTipFactory.create();
toolTip.innerHTML = str;
};
9.6 享元模式小结 享元模式是为解决性能问题而生的模式,在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题;
系列链接
- JavaScript 设计模式(上)——基础知识
- JavaScript 设计模式(中)——1.单例模式
- JavaScript 设计模式(中)——2.策略模式
- JavaScript 设计模式(中)——3.代理模式
- JavaScript 设计模式(中)——4.迭代器模式
- JavaScript 设计模式(中)——5.发布订阅模式
- JavaScript 设计模式(中)——6.命令模式
- JavaScript 设计模式(中)——7.组合模式
- JavaScript 设计模式(中)——8.模板方法模式
- JavaScript 设计模式(中)——9.享元模式
- JavaScript 设计模式(中)——10.职责链模式
- JavaScript 设计模式(中)——11. 中介者模式
- JavaScript 设计模式(中)——12. 装饰者模式
- JavaScript 设计模式(中)——13.状态模式
- JavaScript 设计模式(中)——14.适配器模式
- JavaScript 设计模式(下)——设计原则
- JavaScript 设计模式练习代码
本文主要参考了《JavaScript设计模式和开发实践》一书
推荐阅读
- 热闹中的孤独
- Shell-Bash变量与运算符
- JS中的各种宽高度定义及其应用
- 2021-02-17|2021-02-17 小儿按摩膻中穴-舒缓咳嗽
- 深入理解Go之generate
- 异地恋中,逐渐适应一个人到底意味着什么()
- 我眼中的佛系经纪人
- 《魔法科高中的劣等生》第26卷(Invasion篇)发售
- “成长”读书社群招募
- 2020-04-07vue中Axios的封装和API接口的管理