《js 设计模式与开发实践》读书笔记 12
享元 flyweight 模式是一种性能优化模式,享元可以理解成共享元对象。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式的概念听起来不太好理解,我们来看一个例子。
假设爱慕的产品。有 59 种男士内衣,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()
}
// 修改
var Model = function (sex, underwear) {
this.sex = sex
}
Model.prototype.takePhoto = function () {
console.log('sex= ' + this.sex + ' underwear= ' + this.underwear)
}
var maleModel = new Model('male')
var femaleMode = 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()
}
改进之后,就是只需要两个对象就可以了。不需要重复创建很多对象。上面的代码是享元模式的雏形,它要求将对象的属性划分为内部属性和外部属性。享元模式的目标是尽量减少共享对象的数量。关于如何划分内部状态和外部状态。可以通过几点来做区分。1 内部状态存储于对象内部。2 内部状态可以被一些对象共享。3 内部状态独立于具体的场景,通常不会改变。4 外部状态取决于具体的场景,并根据场景而变化。外部状态不能背共享。(完)通过拆分内部和外部属性,虽然组合外部属性时会花费一些时间,但可以减少系统中的对象数量。所以这是一种时间换空间的优化模式。
上面代码中有两个问题。1 我们通过 new 了两个 model 对象,在其他系统中,也许并不是一开始就需要所有的共享对象。2 给 model 对象设置了 underware 的外部属性,在复杂系统中,这不是一个好的方式。我们通过对象工厂来解决第一个问题,第二个问题,可以用一个管理器来记录对象相关的外部状态。使这些外部状态通过某个钩子和共享对象联系起来。
我们讲下对象池,对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是之间 new,而是从对象池中获取。如果对象池里么有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待下次获取。对象池技术的应用非常广泛,http 连接池和数据库连接池都是其代表应用。在 web 前端开发中,对象池使用最多的场景大概就是跟 dom 有关的操作。假设我们开发一个地图应用,地图上经常会出现一些标志地名的小气泡,我们叫他 toolTip。比如搜索我们附近时出现两个小气泡,搜索兰州拉面的时候,出现了 6 个。按照对象池的思想,在第二次搜索开始之前,不会把第一次创建的 2 个小气泡删除掉,而是把他们放进对象池,这样第第二次的搜索结果页面里,我们只需要再创建 4 个小气泡而不是 6 个。我们先定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包里,这个工厂有两个暴露对外的方法,create 表示获取一个 div 节点,recover 表示回收一个 div 节点。
var toolTipFactory = (function () {
var toolTipPool = []
return {
create: function () {
if (toolTipPool.length === 0) {
var div = document.createElement('div')
document.body.appendChild(div)
return div
} else {
return toolTipPool.shift()
}
},
recover: function (tooltipDom) {
return toolTipPool.push(tooltipDom)
}
}
})()
var ary = []
var str = ['a', 'b']
for (var i = 0; i < str.length; i++) {
var toolTip = toolTipFactory.create()
toolTip.innerHTML = str[i]
ary.push(toolTip)
}
接下来假设地图需要开始重新绘制,在此之前需要把两个节点回收到对象池,然后再生成 6 个气泡。
document.getElementById('button1').onclick = function () {
for (let i = 0; i < ary.length; i++) {
toolTipFactory.recover(toolTip)
}
let str = ['a', 'b', 'c', 'd', 'e', 'f']
for (let i = 0; i < str.length; i++) {
const el = str[i]
let tt = toolTipFactory.create()
tt.innerHTML = el
}
}
现在再测试一番,页面中出现了6个节点。对象池和享元模式的思想有点相似,虽然这里没有粉笔内部属性和外部属性。以后遇到大量相似对象的系统,享元模式的思想可以给我们解决性能问题。