JavaScript 享元模式
前言
享元模式是一种性能优化的模式,它通过共享对象的方式,减少实例对象的数量,它将对象的属性拆分为内部属性和外部属性,内部属性是可以共享的,外部属性是无法共享的,我们将共享的部分封装到对象中,然后大家共用这一个对象,通过方法或者直接赋值外部属性给这个对象,从而实现对象的复用。
但是赋值的过程是会耗时的,这就是典型的时间换空间的优化方式了。
享元模式是一种结构型设计模式,旨在减少应用程序中的内存使用或计算开销,通过共享对象实例来优化性能。
初识享元模式
假设有个内衣工厂,目前的产品有 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();
};
想要得到一张照片,就得new一个新的model对象实例,如果我们将来需要100000个照片,就得new出这么多对象,显然会产生性能上的问题。
那么如何优化这个场景,显然我们不需要那么多模特对象,其实男女模特只需要各一个就行了,然后只需要让他们各自穿不同的衣服拍照即可。
改造一下代码:
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();
};
可以看到,现在我们只需要2个实例对象就完成了该功能。
内部状态和外部状态
享元模式将对象的属性分为内部状态和外部状态,那么如何区分内部和外部:
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体的场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
说白了就是将不变与变的部分拆分出来,不变的放在共享对象中,变的作为外部状态,在拿到共享对象后进行赋值。
文件上传的例子
// 文件享元工厂
const FileFlyweightFactory = (function() {
const files = {}; // 保存已创建的文件享元实例
return {
getFile: function(name, size, type) {
const fileKey = name + size + type; // 创建文件实例的唯一标识
if (!files[fileKey]) {
files[fileKey] = {
name,
size,
type,
uploaded: false
};
}
return files[fileKey];
}
};
})();
// 示例代码
const file1 = FileFlyweightFactory.getFile("text.txt", 1024, "text/plain");
const file2 = FileFlyweightFactory.getFile("text.txt", 1024, "text/plain"); // 重复的文件
console.log(file1); // { name: 'text.txt', size: 1024, type: 'text/plain', uploaded: false }
console.log(file2); // { name: 'text.txt', size: 1024, type: 'text/plain', uploaded: false }
当我们在实现文件上传的时候,一般都需要创建一个用于上传的对象,这个对象假设就包含了name、size、type、uploaded
,其中uploaded
是固定的,所以它是内部属性,其他的是外部属性。
并且通过工厂方法,可以便捷的创建共享对象和获取到可复用的对象。
但是享元模式也有一个很大的弊端,从代码中我们可以看到,它只能适用于单线程的逻辑,如果我先要并行获取到共享对象并使用它,就会导致其他地方发生问题,它必须是一步一步来,用完了才可以给下一个人使用。
对象池
对象池和享元模式不是同一个东西,但是他们都实现了对象的复用,但是对象池更加侧重于管理可复用的对象实例(创建和销毁),而享元模式更关注需要大量共享对象实例的场景。
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
}
}
})();
可以看到,对象池是支持并行的,相对于传统的享元模式,它更加适合需要大量实例的情况,且不关心实例什么时候使用完毕,不够直接创建即可。
它的应用场景在游戏中较多,比如飞机大战的子弹,如果使用享元模式,你需要等待子弹发射到屏幕外才能再次复用,显然不符合使用场景,使用对象池就可以快速创建多个子弹,在子弹超出屏幕外后再回收回来复用。
通用对象池
var objectPoolFactory = function(createObjFn) {
var objectPool = [];
return {
create: function() {
var obj = objectPool.length === 0 ?
createObjFn.apply(this, arguments) : objectPool.shift();
return obj;
},
recover: function(obj) {
objectPool.push(obj);
}
}
};
var iframeFactory = objectPoolFactory(function() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
iframe.onload = function() {
iframe.onload = null; // 防止 iframe 重复加载的 bug
iframeFactory.recover(iframe); // iframe 加载完成之后回收节点
}
return 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);
组合使用
// 创建对象池工厂
const ObjectPoolFactory = (function() {
const pool = [];
return {
createObject: function(key) {
const object = {
key,
inUse: false,
operation() {
console.log(`Performing operation for key: ${key}`);
}
};
pool.push(object);
return object;
},
getAvailableObject: function() {
return pool.find(obj => !obj.inUse);
},
recover(obj) {
pool.push(obj);
}
};
})();
// 使用对象池工厂支持并行操作
const objects = [];
for (let i = 0; i < 10; i++) {
const object = ObjectPoolFactory.createObject(`key${i}`);
objects.push(object);
}
objects.forEach((object, index) => {
setTimeout(() => {
object.inUse = true;
object.operation();
object.inUse = false;
ObjectPoolFactory.recover(obj);
}, index * 1000);
});
我们可以将享元模式与对象池组合使用,以适应更加复杂的业务场景。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据