js中的享元模式
定义
运用共享技术有效支持大量细粒度的对象,以减少对象的创建数量来减少内存的占用提高性能。
详细描述
享元模式的核心就是共享,当在项目开发中创建了太多的对象,而这些对象还有很多相似之处的时候,我们就会把相似的对象提取出来让这些业务共用同一个对象来实现,以达到减少对内存的使用提高性能,这就是享元模式。
享元模式的目标是尽量减少共享对象的数量,它要求将对象的状态区分为内部状态和外部状态(状态就是属性)。内部状态就是可以被所有对象共享的状态,它存储在对象内部不会随着使用场景改变而改变。外部状态就是存储在外部的状态,会随着具体的使用场景而改变,不能被对象共享,在必要的时候会传入共享对象来组成一个完整的对象。一般来说内部状态有多少种就会有多少个对象。
区分内部状态和外部状态总结:
- 内部状态存储于对象内部
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
还有一种和享元模式非常非常相似的技术-对象池。它也是维护一个已经创建好的对象集,当需要使用对象时不用重新创建新对象直接从原来对象池中取出即可。它和享元模式的主要区别就是不需要剥离内部状态和外部状态。
代码实例
假设有一个批发商批发了一批内衣,成人男女款式各有50种,现在需要请一些模特穿上内衣拍照制成广告宣传。下面首先看下不使用享元模式的实现:
class Model {
constructor(style, underwear) {
this.style = style;
this.underwear = underwear;
}
takePhoto() {
console.log(this.style + '-' + this.underwear);
}
}
// 成人男内衣
for (let index = 1; index <= 50; index++) {
const maleModel = new Model('成人男士', '内衣款式' + index);
maleModel.takePhoto();
}
// 成人女内衣
for (let index = 1; index <= 50; index++) {
const femaleModel = new Model('成人女士', '内衣款式' + index);
femaleModel.takePhoto();
}
上述代码是不使用享元模式的实现,每种款式的内衣拍照我们找了一个模特(创建了一个对象)这样我们一共找了100个模特来拍照,显然这样会造成巨大的资源浪费。
我们从上面创建的对象可以发现模特类型(style)状态可以在多个对象中进行共享,内衣款式状态underwear根据外部使用而改变,所以我们可以进行状态的拆分将style拆分为内部状态,underwear为外部状态,拆分完状态后我们来看下使用共享模式是怎样实现的:
class Model {
constructor(style) {
// 拆分内部状态进行公用
this.style = style;
}
takePhoto() {
console.log(this.style + '-' + this.underwear);
}
}
// 创建共享对象,传入共享内部状态
const maleModel = new Model('成人男士'),
femaleModel = new Model('成人女士');
// 成人男内衣
for (let index = 1; index <= 50; index++) {
maleModel.underwear = `内衣款式${index}`;
maleModel.takePhoto();
}
// 成人女内衣
for (let index = 1; index <= 50; index++) {
femaleModel.underwear = `内衣款式${index}`;
femaleModel.takePhoto();
}
上述代码就是拆分完内部状态style和外部状态underwear后使用享元模式的实现,同样的结果我们只需要创建两个模特对象就可以实现,大大的节省了内存。
上面情况是我们知道内衣款式于是先花钱请了两个模特,然后再进行拍照,但是有些情况下我们的订单量实现是太大了也不知道具体来的款式有哪些,是不是还有小孩的内衣等等,所以这个时候上面的场景就不够通用不能满足我们其他的需求,我们就不能预先创建好对象而是根据使用场景来进行创建,下面实现一下更加通用的享元模式:
class Model {
constructor(style) {
// 拆分内部状态进行公用
this.style = style;
console.log('对象创建');
}
takePhoto() {
console.log(this.style + '-' + this.underwear);
}
}
// 对象创建的工厂
const modelFactory = (() => {
const createdModelObjs = {};
return {
create(styleType) {
if (createdModelObjs[styleType]) return createdModelObjs[styleType];
return (createdModelObjs[styleType] = new Model(styleType));
},
};
})();
// 内衣数据
const underwearDatas = [
{
styleType: '成人男士',
underwear: 'underwear1',
},
{
styleType: '成人男士',
underwear: 'underwear2',
},
{
styleType: '成人女士',
underwear: 'underwear3',
},
{
styleType: '成人女士',
underwear: 'underwear4',
},
{
styleType: '儿童女士',
underwear: 'underwear5',
},
];
underwearDatas.forEach(({ styleType, underwear }) => {
const model = modelFactory.create(styleType);
model.underwear = underwear;
model.takePhoto();
});
上述代码定义了一个创建共享对象的工厂函数,并且可以维护已经已经创建的共享对象,在我们不知道到货情况的时候就可以使用这个工厂函数根据实际到货情况动态的创建和使用共享对象。仔细看它有点类似于单例模式,但是因为有剥离外部和内部状态的过程所以依然是享元模式。
对象池
对象池技术也是一个利用共享的技术,它维护了一些空闲状态下的对象,当需要用到的时候直接从池子里取不需要重新new,如果池子里面没有空闲对象的时候就创建一个新对象使用,当取出的对象完成使用后,在放入池子等待下次被使用。
例如我们在点击屏幕的时候四周会有泡泡闪动的效果,有时候一个有时候多个,他们的形状和闪动效果都一样只有颜色不同,每个泡泡可以看做是一个dom来实现,为了防止他们随着点击不停的创建销毁,创建销毁造成较大的开销,所以就可以使用对象池技术来优化。
下面我们来实现一个泡泡闪动的例子:
const domFactory = (() => {
const domPool = []; // dom对象池
return {
create() {
if (domPool.length) return domPool.shift();
const div = document.createElement('div'); // 创建一个 泡泡
document.body.appendChild(div);
// ....这里添加泡泡具体大小形状等等
div.style.width = '50px';
div.style.height = '50px';
div.style.borderRadius = '50%';
return div;
},
recover(bubbleDom) {
bubbleDom.style.backgroundColor = 'transparent';
return domPool.push(bubbleDom); // 对象池回收泡泡
},
};
})();
const bubbleList = []; // 存储泡泡
const colorList = ['yellow', 'blue', 'green', 'red', 'black'];
// 点击时出现泡泡
document.body.onclick = function () {
// 回收使用完成的泡泡
while (bubbleList.length) {
domFactory.recover(bubbleList.shift());
}
const num = Math.floor(Math.random() * 5 + 1);
for (let index = 0; index < num; index++) {
console.log(num, index);
const bubble = domFactory.create();
bubble.style.backgroundColor = colorList[index]; // 设置不同的颜色
// 这里还可以设置不同的位置等等 ...
bubbleList.push(bubble);
}
};
上述代码实现了当点击的时候随机出现1-5个泡泡,每个泡在消失的时候不会销毁重建而是会被对象池回收,下次再出现的时候直接复用即可,尤其是界面上有很多很多这样的数据时就可以节省很大的开销。但是因为维护对象池本身也需要一定的资源,所以当维护对象池消耗的资源小于创建销毁的消耗时才考虑使用对象池技术。
使用场景
- 一个程序中使用了大量的相似或相同的对象,这些对象的使用占用了大量的内存。
- 对象的大部分状态都可以外部化。
- 剥离出外部状态后,可以用较少的共享对象替代大量对象。
- 大量对象占用的内存小于维护享元池的内存时。
总结
享元模式是一种使用时间换空间的性能优化模式,它的优缺点也存在于此,节省空间的同时在时间上有了额外的花费。
优点
- 享元模式可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
- 享元模式的外部状态相对独立,不会影响其内部状态,所以可以适用各种不同的场景。
缺点
- 剥离内外部状态时需要花费时间。
- 内部状态和外部状态的剥离使逻辑更加抽象,系统程序更加复杂,理解时间更长。