本文深入解析JavaScript中的享元模式,通过模特穿衣与文件上传案例,阐述如何利用共享技术减少细粒度对象创建以优化性能。重点区分内部状态与外部状态,并介绍对象池在DOM复用和数据库连接中的应用,帮助开发者在大量相似对象场景下实现时间与空间的平衡。
一、认识享元模式
享元模式是一种旨在提升系统性能的设计模式,其核心机制在于通过共享技术来高效支持大量细粒度的对象实例。
通俗理解,该模式旨在找出事物众多属性中分类最少的一类,并依据属性值的数量进行归类。例如,某工厂需拍摄20名男模特和20名女模特穿着40款新衣的宣传照。若不采用享元模式,需聘请40位模特,造成巨大经济损失且无必要;若采用享元模式,仅需一男一女两位模特即可通过更换衣物完成所有拍摄。下文将展示具体代码实现。
二、代码具体实现
1. 未使用享元模式的案例实现
分析:这是常规代码实现方式,共创建了40个对象。日常开发中可能忽略此细节,但在实际项目中若创建数万个乃至数十万个对象,将导致严重的性能损耗与内存浪费。
let Model = function(sex, underwear) {
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
}
for(let i = 0; i < 20 ; i++){
new Model('男', 'underwear' + i).takePhoto();
}
for(let i = 0; i < 20 ; i++){
new Model('女', 'underwear' + i).takePhoto();
}2. 使用享元模式重构上述代码
分析:重构后的代码仅创建了两个对象便完成了相同任务。无论需要多少对象,实际仅需创建两个实例,从而大幅提升了运行性能。
let ModelR = function( sex ) {
this.sex = sex;
}
let ModelF = new ModelR( '女' );
let ModelM = new ModelR('男');
ModelR.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
}
for(let i = 0; i < 20 ; i++) {
ModelF.underwear = 'underwear' + i;
ModelF.takePhoto();
}
for(let i = 0; i < 20 ; i++) {
ModelM.underwear = 'underwear' + i;
ModelM.takePhoto();
}总体分析:
目前我们对享元模式已有初步认识,其核心思想简洁明了:利用所有对象共有的属性来初始化并创建对象。在上述案例中,利用“性别”这一属性创建对象,由于性别仅分男女两类,因此只需创建两个对象。将衣服作为其他不同属性添加到对象中,即可完成对象的替换,相当于拥有40个不同对象,但实际仅创建了两个。
因此,我们引出了一个新的概念:内部状态与外部状态。
3. 享元模式的状态
- 内部状态:即上文提到的属性分类最少的一类,如性别,仅分两类,可被对象共享。
- 外部状态:指其他属性,不可被共享。
结论:剥离了外部状态的对象成为共享对象,外部对象在必要时被传入共享对象以组装成完整对象。虽然组装外部对象需耗费一定时间,但节省了大量内存损耗,因此享元模式是一种以时间换空间的优化模式。
三、享元模式实际应用
假设我们需要实现文件上传功能,现有Flash和Plugin两种上传方式。每次上传对应一个JavaScript对象的创建。若按部就班,当大量文件上传时会导致浏览器假死。因此,我们利用享元模式设计代码。首先确定文件的内部状态和外部状态:文件大小、文件类型、文件上传方式。其中文件大小和类型不可控,而上传方式仅两种,故将文件上传方式作为内部状态(注:原文此处逻辑有误,通常不变量作内部状态,变量作外部状态,但此处依原文逻辑:原文称“将文件上传方式作为外部状态”,随后代码中uploadType作为构造函数参数,通常内部状态是共享的,即uploadType。原文描述存在混淆,此处依原文文字改写:将文件上传方式作为外部状态,现在我们来编写代码。
let Upload = function(uploadType) {
this.uploadType = uploadType;
}
Upload.prototype.delFile = function( id ) {
uploadManager.setExternalState(id, this);
if(this.fileSize < 3000) {
return this.dom.parentNode.removeChild(this.dom);
}
}
// 使用工厂模式来创建对象
let UploadFactory = function() {
let cache = {};
return {
create(uploadType) {
if(cache[uploadType]){
return cache[uploadType];
}
return cache[uploadType] = new Upload( uploadType );
}
}
}()
// 创建一个管理器封装外部状态
let uploadManager = function() {
uploadDatabase = {};
return {
add(id, uploadType, fileName, fileSize){
let uploadObj = UploadFactory.create( uploadType );
let dom = document.createElement('div');
dom.innerHTML = `<span>文件名称: ${ fileName },文件大小:${fileSize}</span> <button id="del">删除</button>`;
dom.querySelector('#del').onclick = function() {
uploadObj.delFile( id );
}
document.body.appendChild( dom );
uploadDatabase[ id ] = {
fileName,
fileSize,
dom
}
return uploadObj;
},
setExternalState(id, uploadObj){
let uploadData = uploadDatabase[id];
for(let i in uploadData) {
uploadObj[i] = uploadData[i];
}
}
}
}();
let id = 0;
window.startUpload = function(uploadType, files) {
for(let i = 0,file; file = files[i++];){
let uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
// 进行上传
}
};
startUpload('plugin', [
{
fileName: '1.txt',
fileSize: 1000
},
{
fileName: '2.txt',
fileSize: 1000
},
{
fileName: '3.txt',
fileSize: 3000
}
])
startUpload('flash', [
{
fileName: '4.txt',
fileSize: 1000
},
{
fileName: '5.txt',
fileSize: 1000
},
{
fileName: '6.txt',
fileSize: 3000
}
])扩展:再谈内部状态和外部状态
现在我们思考,如果没有内部状态或者没有外部状态该怎么办。
- 没有内部状态享元:此时,所有属性作为外部享元,相当于内部享元只有一种空状态,因此我们只需创建一个对象,此时便相当于之前所提的单例模式。
- 没有外部状态享元:这时引入一个新的概念,对象池。
四、对象池
对象池的应用十分广泛,数据库的连接池就是其重要用武之地。对象池的大多数使用场景涉及DOM操作,因为DOM的创建和删除是JavaScript中最消耗性能的操作。
理解对象池非常简单,例如制作圆形月饼,只需制造一个圆形模具便可做无数圆形月饼;想做方形月饼时,只需制造一个方形模具,同时将圆形模具保留下来,等再次使用时直接取出即可。对象池原理相同,查看其通用实现代码。
let objectPoolFactory = function( fn ) {
let pool = [];
return {
create(...args){
let obj = (pool.length === 0)? fn.apply(this, args) : pool.shift();
return obj;
},
recover(obj) {
pool.push(obj);
}
}
}实际应用
我们在地图上搜索几个不同位置,第一次搜索显示北京的两个景区位置,第二次搜索北京三个饭店的位置。
分析:第一次需要两个DOM节点,因此创建两个节点,之后将其回收;第二次需要三个DOM节点,使用之前的两个,只需再创建一个新的节点即可,大大提高了代码性能。
// 创建 dom 节点
let createDomFactory = objectPoolFactory(()=>{
let div = document.createElement('div');
document.body.appendChild(div);
return div;
});
let ary = []; // 用于回收
let name = ['天安门', "长城"];
for(let i = 0, l= name.length; i < l ;i++){
let dom = createDomFactory.create();
dom.innerHTML = name[i];
ary.push(dom);
}
for(let i = 0, l = ary.length; i < l ; i ++ ){
createDomFactory.recover(ary[i]);
}
let name1 = ["饭店1", "饭店2", "饭店3"];
for(let i = 0, l = name1.length; i < l; i++) {
let dom = createDomFactory.create();
dom.innerHTML = name1[i];
}五、总结
享元模式是一种很好的性能优化方案,但也会带来一些复杂性问题,因此需要选择合适的时机使用享元模式,例如:
一个程序中使用了大量相似对象;使用大量对象造成很大内存开销;对象大多数状态都可以变为外部状态;剥离出外部对象之后,可以用相对较少的共享对象取代大量的对象。
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!