定义

享元模式是一种用于性能优化的模式。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

# 案例:模特穿衣

某衣服工厂生产了50种男式衣服和50种女式衣服,现在需要塑料模特来穿上衣服拍照。正常情况下需要50个男模特和50个女模特,然后每人分别穿上衣服拍照。
传统实现思路:创建模特类,构造函数参数为性别sex和衣服underwear。然后循环100次,实例化100个对象,然后分别用掉穿衣和拍照方法。
传统实现思路的问题在于对象爆炸,如果有1000套衣服,那就会实例化1000个对象,程序可能会因为对象太多而崩溃。
享元模式实现思路:虽然有100种衣服,但是拍照只需要男女模特各一个就足够了。把衣服underwear参数从构造函数中移除,构造函数只接收sex参数。

var Model = function (sex) {
  this.sex = sex
}

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

var maleModel = new Model('male')

var femalModel = new Model('female')

for (var i = 0; i <= 50; i++) {
  maleModel.underwear = 'underwear' + i
  maleModel.takePhoto()
}

for (var i = 0; i <= 50; i++) {
  femaleModel.underwear = 'underwear' + i
  femaleModel.takePhoto()
}

# 内部状态与外部状态

享元模式要求将对象的属性划分为内部状态与外部状态。享元模式的目标是尽量减少共享对象的数量。
区分内部状态和外呼状态

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

模特穿衣例子中,性别是内部状态,衣服是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。

# 案例:文件上传

微云的文件上传功能可以选择队列,一个一个地排队上传,支持同时选择2000个文件。每一个文件都对应着一个JavaScript上传对象。如果创建2000个upload对象,chrome还能勉强支撑,IE肯定原地爆炸。 享元模式实现

var 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);
  }
}

var UploadFactory = (function () {
  var createFlyWeightObjs = {};
  return {
    create: function (uploadType) {
      if (createFlyWeightObjs[uploadType]) {
        return createFlyWeightObjs[uploadType];
      }
      return createFlyWeightObjs[uploadType] = new Upload(uploadType);
    }
  }
})();

var uploadManager = (function () {
  var uploadDatabase = {};
  return {
    add: function (id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);

      var 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) {
      var uploadData = uploadDatabase[id];
      for (var i in uploadData) {
        flyWeightObj[i] = uploadData[i];
      }
    }
  }
})()

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

startUpload("plugin", [{
    fileName: "1.txt",
    fileSize: 1000
  },
  {
    fileName: "2.html",
    fileSize: 3000
  },
  {
    fileName: "3.txt",
    fileSize: 5000
  }
]);
startUpload("flash", [{
    fileName: "4.txt",
    fileSize: 1000
  },
  {
    fileName: "5.html",
    fileSize: 3000
  },
  {
    fileName: "6.txt",
    fileSize: 5000
  },
])

# 享元模式的适用性

享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。

  • 一个程序中使用了大量的相似对象
  • 由于使用了大量对象,造成了很大的内存开销
  • 对象的大多数状态都可以变为外部状态

# 对象池

对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新对象。

通用对象池的实现

var objectPoolFactory = function (createObjFn) {
  var objectPool = []
  
  return {
    create: function () {
      var obj = objectPool.length === 0 ? createObjFn.apply(this, arguments) : objectPool.shift()

      return obj
    },

    recover: function (obj) {
      objectPool.push(obj)
    }
  }
}

对象池是另外一种性能优化方案,它跟享元模式有一些相似之处,但没有分离内部状态和外部状态这个过程。

# 小结

享元模式是为解决性能问题而生的模式,这跟大部分模式的诞生原因都不一样。在一个存在大量相似对象的系统中,享元模式可以很好的解决大量对象带来的性能问题。