享元(flyweight)模式是一种用于性能优化的模式,fly在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。
如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。在JavaScript中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存就成了一件非常有意义的事情。
享元模式的概念初听起来并不太好理解,所以在深入讲解之前,我们先看一个例子。
初识享元模式
假设有个内衣工厂,目前的产品有50种男士内衣和50种女士内衣,为了推销产品,工厂决定找一些模特来穿上他们的内衣拍成广告照片。正常情况下需要50个男模特和50个女模特,然后让他们每人分别穿上一件内衣来拍照。不使用享元模式的情况下,在程序里也许会这样写:
type Sex = 'male' | 'female'
class Model {
sex: Sex
underwear: string
constructor(sex, underwear) {
this.sex = sex
this.underwear = underwear
}
takePhoot() {
console.log(`sex=${this.sex}underwear=${this.underwear}`)
}
}
for (let i = 1; i <= 50; i++) {
const maleModel = new Model('male', 'underwear' + i)
maleModel.takePhoot()
}
for (let i = 1; i <= 50; i++) {
const femaleModel = new Model('female', 'underwear' + i)
femaleModel.takePhoot()
}
要得到一张照片,每次都需要传入sex和underwear参数,如上所述,现在一共有50种男内衣和50中女内衣,所以一共会产生100个对象。如果将来生产了10000种内衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。
下面我们来考虑一下如何优化这个场景。虽然有100种内衣,但很显然并不需要50个男模特和女模特。其实男模特和女模特各自有一个就足够了,他们可以分别穿上不同的内衣来拍照。
现在来改写一下代码,既然只需要区别男女模特,那我们先把underwear参数从构造函数中移除,构造函数只接受sex参数:
class ModelOpt {
sex: Sex
underwear: string
constructor(sex) {
this.sex = sex
}
takePhoot() {
console.log(`sex=${this.sex}underwear=${this.underwear}`)
}
}
const maleModelOpt = new ModelOpt('male'),
femaleModelOpt = new ModelOpt('female')
for (let i = 1; i <= 50; i++) {
maleModelOpt.underwear = 'underwear' + i
maleModelOpt.takePhoot()
}
for (let j = 1; j <= 50; j++) {
femaleModelOpt.underwear = 'underwear' + j
femaleModelOpt.takePhoot()
}
可以很明显的看到,我们只需要分别创建一男一女两个对象,并对他们进行重复使用,就实现了我们的功能。
内部状态和外部状态
上面的那个例子就是享元模式的雏形,享元模式的宗旨就是将对象的状态分为内部状态和外部状态以达到我们尽量减少创建对象的目的。
那么关于如何去划分内部状态和外部状态,我在这里可以给出一些建议:
- 内部状态存储于对象内部。
- 内部状态可以被一些对象共享。
- 内部状态独立于具体场景,通常不会改变。
- 外部状态取决于具体的场景,并根据场景而变化,且外部状态不能被共享。
按照上面所说,我们可以把所有内部状态相同的对象都指定为同一个共享对象,对于外部状态可以对象上剥离出来并存储在外部。
剥离了外部状态的对象就成为了共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部对象需要花费一定的时间,但是却可以大大的减少内存中所存在的对象数量,因此享元模式是一种空间换时间的优化模式。
使用享元模式最最最关键的地方是在于如何区分内部状态和外部状态。可以被对象共享的状态通常被认为是内部状态,就好像不管什么样子的衣服,都可以按照性别的不同,穿在对象的男模特或者女模特身上,模特的性别就可以作为内部状态存储在模特这个对象内部。而外部状态取决于场景,并且根据场景而变化,就像例子中模特身上穿的衣服都是不同的,它们并不能被一些对象共享,因此被划分为外部对象。
享元模式的适用性
享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题。享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式:
- 一个程序中使用了大量的相似对象
- 由于使用了很多对象,造成了很大的内存开销
- 对象的大多数状态都可以变为外部状态
- 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量的对象。
小结
享元模式是为了解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好地解决大量对象带来的性能问题。
在现代化前端代码编写的过程中,因为我们大多数的情况下都会使用前端框架进行页面的编写诸如React/Vue,在这些框架中会有虚拟dom的存在,它可以一定程度上缓解我们没有用享元模式而出现大量对象的情况。
但是如果我们用的是node.js编写服务端的代码,那么享元模式不失为一个优化代码和逻辑思路的利器的。