享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为“蝇量级”。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在 JavaScript 中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
故事背景
假设有个内衣工厂,目前的产品有 50 种男式内衣和 50 种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。
代码实现(未使用享元模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class Model { constructor(sex, underwear) { this.sex = sex; this.underwear = underwear; }
takePhoto() { console.log('sex= ' + this.sex + ' underwear=' + this.underwear); }; }
for (let i = 1; i <= 50; i++) { let maleModel = new Model('male', 'underwear' + i); maleModel.takePhoto(); }
for (let j = 1; j <= 50; j++) { let femaleModel = new Model('female', 'underwear' + j); femaleModel.takePhoto(); }
|
思考:真的需要如此多数量的对象吗?
如上所述,现在一共有 50 种男内 衣和 50 种女内衣,所以一共会产生 100 个对象。如果将来生产了 10000 种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。 下面我们来考虑一下如何优化这个场景。虽然有 100 种内衣,但很显然并不需要 50 个男模特和 50 个女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
代码重构(享元模式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
class Model { constructor(sex) { this.sex = sex; }
takePhoto() { console.log('sex= ' + this.sex + ' underwear=' + this.underwear); }; }
let maleModel = new Model('male'), femaleModel = new Model('female');
for (let i = 1; i <= 50; i++) { maleModel.underwear = 'underwear' + i; maleModel.takePhoto(); }
for (let j = 1; j <= 50; j++) { femaleModel.underwear = 'underwear' + j; femaleModel.takePhoto(); }
|
只需要两个对象便完成了同样的功能
如何使用享元模式
享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引
- 内部状态可以被一些对象共享
- 内部状态独立于具体的场景,通常不会改变
- 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
在上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女两种,所以该内衣厂商最多只需要 2 个对象。
享元模式的通用结构
问题引入
- 我们通过构造函数显示
new
出了男女两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象
- 给
model
对象手动设置了underwear
外部状态,在更复杂的系统中,这不是一个最好的方式,因为外部状态可能相当复杂,它们与共享对象的联系会变得困难
解决方案
第一个问题可以用工厂模式解决,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象
第二个问题可以用一个管理器记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来
享元模式的替代方案 —— 对象池
对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接 new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入池子等待被下次获取。
通用的对象池实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let objectPoolFactory = function (createObjFn) { let objectPool = [];
return { create: function () { return objectPool.length === 0 ? createObjFn.apply(this, arguments) : objectPool.shift(); },
recover: function (obj) { objectPool.push(obj); }, }; };
|
现在利用objectPoolFactory
来创建一个装载一些 iframe 的对象池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let iframeFactory = objectPoolFactory(function () { let iframe = document.createElement("iframe"); document.body.appendChild(iframe); iframe.onload = function () { iframe.onload = null; iframeFactory.recover(iframe); }; return iframe; });
let iframe1 = iframeFactory.create(); iframe1.src = "http:// baidu.com";
let iframe2 = iframeFactory.create(); iframe2.src = "http:// QQ.com";
setTimeout(function () { let iframe3 = iframeFactory.create(); iframe3.src = "http:// 163.com"; }, 3000);
|
对象池的应用非常广泛,HTTP连接池和数据库连接池都是其代表应用,也可以用在dom的重复利用上。
参考
《javascript设计模式与开发实践》