Skip to content

享元模式(Flyweight)

享元模式是一种用于性能优化的模式

享元模式的核心是运用共享技术来有效支持大量细粒度的对象

享元模式的过程是剥离外部状态,并把外部状态保存在其他地方,在合适的时刻再把外部状态组装进共享对象

享元模式的适用性

  1. 一个程序中使用了大量的相似对象
  2. 由于使用了大量对象,造成很大的内存开销
  3. 对象的大多数状态都可以变成外部状态
  4. 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象

缺点 (进阶中有举例)

  • 维护多一份外部状态对象与工厂对象的开销

初识

假设目前有产品50种男生内裤和50种女生女裤,现在要生产一些塑料模特来穿上它们拍照用于广告宣传。正常情况下,需要50个男模特和50个女模特,然后分别让它们每人穿上一条内裤来拍照。

javascript
const Model = function(sex, underwear) {
  this.sex = sex;
  this.underwear = underwear;
}

Model.prototype.takePhoto = function() {
  console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
}

for (let i = 1; i <= 50; i++ ){    
  var maleModel = new Model( 'male', 'underwear' + i )
  maleModel.takePhoto()
}

// 女模特同上

这时我们会发现一个问题,目前一共产生了100个对象,如果将来要生产10000种呢?这程序很可能会崩溃掉。

其实男模特和女模特各要一个即可,让它们分别穿上不同的内裤拍照就好了。

javascript
const Model = function (sex) {
  this.sex = sex
}

Model.prototype.takePhoto = function () {
  console.log('sex= ' + this.sex + ' underwear=' + this.underwear);
}

const 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()
}

然而这就是一个简单的享元模式雏形。

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。

在上面的例子,性别就是内部状态,内衣就是外部状态。

享元模式的目标是尽量减少共享对象的数量

  • 内部状态存储于对象内部。
  • 内部状态可以被一些对象共享。
  • 内部状态独立于具体的场景,通常不会改变。
  • 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。

外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然会消耗一定时间,却可以大大减少系统中的对象数量。因此,享元模式是一种用时间换空间的优化模式

进阶

在上例子,问题一,我们通过构造函数显示 new 出两个对象,但在其他系统可能不是一开始就需要的。问题二,我们手动设置 underwear 外部状态,这是基于外部状态比较简单的情况下,如果外部状态相当复杂,则与共享对象的联系就会变得困难。

解决方法

  1. 使用对象工厂,只有当某种共享对象被真正需要时,才从工厂被创建出来。
  2. 用管理器来记录对象相关的外部状态。

:现在有一个上传功能,支持同时选择2000个文件,同时支持3种上传方式:插件上传、Flash上传、表单上传。

分析:如果在不使用模式情况下,同时 new 2000个上传对象出来,往往浏览器就出现假死状态。

第一步剥离外部状态

javascript
const Upload = function (uploadType) {
  this.uploadType = uploadType
}

Upload.prototype.delFile = function (id) {
  uploadManager.setExternalState(id, this)

  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom)
  }

  if (window.confirm('确定删除该文件吗?' + this.fileName)) {
    return this.dom.parentNode.removeChild(this.dom)
  }
}

第二步工厂进行对象实例化

javascript
const UploadFactory = (function () {
  const createFlyWeightObjs = {}

  return {
    create: function (uploadType) {
      if (createFlyWeightObjs[uploadType]) {
        return createFlyWeightObjs[uploadType]
      }

      return createFlyWeightObjs[uploadType] = new Upload(uploadType)
    }
  }
})()

第三步管理器封装外部状态

javascript
const uploadManager = (function () {
  const uploadDatabase = {}

  return {
    add: function (id, uploadType, fileName, fileSize) {
      const flyWeightObj = UploadFactory.create(uploadType)
      const dom = document.createElement('div')
      dom.innerHTML = `<span>文件名称:${fileName}, 文件大小:${fileSize}</span><button class="delFile">删除</button>`
      dom.querySelector('.delFile').onclick = function () {
        flyWeightObj.delFile(id)
      }
      document.body.appendChild(dom)
      uploadDatabase[id] = {
        fileName: fileName,
        fileSize: fileSize,
        dom: dom
      }

        return flyWeightObj
    },
    setExternalState: function (id, flyWeightObj) {
      const uploadData = uploadDatabase[id]
        for (let i in uploadData) {
          // 传入外部状态
          flyWeightObj[i] = uploadData[i]
        }
      }
    }
})()

第四步触发

javascript
let id = 0
window.startUpload = function (uploadType, files) {
  for (let i = 0, file; file = files[i++];) {
    const uploadObj = uploadManager.add(++id, uploadType, file.fileName, file.fileSize)
  }
}

startUpload('plugin', [{
    fileName: '1.txt',
    fileSize: 1000
  },
  {
    fileName: '2.txt',
    fileSize: 3000
  },
  {
    fileName: '3.txt',
    fileSize: 5000
  }
])

startUpload('flash', [{
    fileName: '4.txt',
    fileSize: 1000
  },
  {
    fileName: '5.txt',
    fileSize: 3000
  },
  {
    fileName: '6.txt',
    fileSize: 5000
  }
])

实现效果

现在通过享元模式实现,对象的数量控制为2,如果还是这两种上传模式,即使同时上传3000个文件,还是只创建2个。即有多少种内部状态,就最多存在多少个共享对象

享元模式

延伸

对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后,再进入池子等待被下次获取。

比如:HTTP连接池和数据库连接池,在web开发中,对象池使用最多的场景大概是跟DOM有关的操作。

比如:现在地图软件在你搜索某个地点后,会在相关地点标上小气泡,一开始你搜xx饭店,可能页面上会出现3个小气泡,而再搜索菜鸟驿站的时候周围出现10个小气泡。在对象池的思想里,第一次会创建3个小气泡对象,但在第二次搜索前,不会把这3个小气泡给删除掉,而是回收到对象池,在第二次搜索后,只需再创建7个小气泡即可。

通用对象池

javascript
const objectPoolFactory = function (createObjFn) {
  let objectPool = []
  return {
    // 判断对象池中是否有空闲对象,有则直接使用
    create: function () {
      const obj = objectPool.lenght === 0 ? createObjFn.apply(this, arguments) : objectPool.shift()

      return obj
    },
    // 回收对象
    recover: function () {
      objectPool.push(ohj)
    }
  }
}

// 该方法具体需求具体实现
const iframeFactory = objectPoolFactory(function () {
  const iframe = document.createElement('iframe')
  document.body.appendChild(iframe)

  iframe.onload = function () {
    iframe.onload = null
    iframeFactory.recover(iframe)
  }

  return iframe
})

const iframe1 = iframeFactory.create()
iframe1.src = ''
const iframe2 = iframeFactory.create()
iframe2.src = ''

setTimeout(function () {
  const iframe3 = iframeFactory.create()
  iframe3.src = ''
}, 3000)

Released under the MIT License.