EmberYu / vic-blog

9 stars 0 forks source link

享元模式 #7

Open EmberYu opened 5 years ago

EmberYu commented 5 years ago

享元模式

享元模式(flyweight)是一种用于性能优化的模式,享元模式的核心是运用共享技术来有效支持大量细粒度的对象。 如果系统中因为创建了大量类似的对象而导致内存占用过高,就可以考虑使用享元模式了
享元模式的目标是尽量减少共享对象的数量,将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。关于如何划分内部状态和外部状态,下面有几条指引

这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。 剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量,相比之下,这点时间或许是微不足道的。因此,享元模式是一种用时间换空间的优化模式。
使用享元模式的关键是如何区别内部状态和外部状态。可以被对象共享的属性通常被划分为内部状态,而外部状态取决于具体的场景,并根据场景而变化。

案例

首先我们看一个文件上传的例子

var id = 0;
window.startUpload = function (uploadType, files) {
  for (var i = 0, file; file = files[i++];) {
    var uploadObj = new Upload(uploadType, file.fileName, file.fileSize);
    uploadObj.init(id++);    // 给upload对象设置一个唯一的id
  }
}

当用户选择完文件之后,startUpload函数会遍历files数组来创建对应的upload对象。下面来看看Upload构造函数,它接受3个参数,分别是插件类型、文件名和文件大小。这些信息都已经被插件组装在files数组里返回

var Upload = function (uploadType, fileName, fileSize) {
  this.uploadType = uploadType;
  this.fileName = fileName;
  this.fileSize = fileSize;
  this.dom = null;
}
Upload.prototype.init = function (id) {
  var that = this;
  this.id = id;
  this.dom = document.createElement('div');
  this.dom.innerHTML = '<span>文件名:'+ this.fileName +'</span><button class="delFile">shanchu </button>'
  this.dom.querySelector('.delFile').onclick = function () {
    that.delFile();
  }
  document.body.appendChild(this.dom);
}

为了简化示例,只保留了delFile方法,该方法是判断当被删除的文件小于3000kb时,直接删除,否则弹出提示框

Upload.prototype.delFile = function () {
  if (this.fileSize < 3000) {
    return this.dom.parentNode.removeChild(this.dom);
  } 

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

接下来创建3个插件上传对象和3个Flash上传对象

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
}])

可以看出来,我们上传了几个文件,那么浏览器中就有多少个upload对象。当我们上传文件足够多的时候,就会容易出现内存崩溃的状态

使用享元模式进行重构

重构时首先我们需要确定什么是upload的内部状态,什么是外部状态,根据划分内外状态的关键点,可以判断出uploadType是对象的内部状态,因为upload对象必须依赖uploadType属性才能工作。

剥离外部状态

明确了uploadType作为内部状态之后,我们再把其他的外部状态从构造函数中抽离出来

var Upload = function (uploadType) {
  this.uploadType = uploadType;
}

Upload.prototype.init函数也不再需要,因为upload对象初始化的工作放在了uploadManager.add函数里面,接下来只需要定义Upload.prototype.del函数即可

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

因为我们upload内部没有大小,所以在删除之前,需要读取外部管理器中文件的大小。

工厂进行对象实例化

接下来定义一个工厂来创建upload对象,如果某种内部状态对应的共享对象已经被创建国,那么直接返回这个对象,否则创建一个新的对象:

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

管理器封装外部状态

现在我们来完善前面提到的uploadManager对象,它负责向UploadFactory提交创建对象的请求,并用一个uploadDatabase对象保存所有upload对象的外部状态,以便在程序运行过程中给upload共享对象设置外部状态:

var uploadManager = (function() {
  var uploadDatabase = {};
  return {
    add: function (id, uploadType, fileName, fileSize) {
      var flyWeightObj = UploadFactory.create(uploadType);
      var dom = docuemnt.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];
      }
    }
  }
})();

然后是开始触发上传动作的startUpload函数

var id = 0;
window.startUplaod = function (uploadTypes, files) {
  for (var i = 0, file; file = [files++];) {
    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
}])

重构之前的代码我们创建了六个对象。而通过享元模式重构之后,对象的数量减少为2,而且对于同一种uploadType,不论增加多少个文件,始终都是一个对象。

鸣谢

本文copy于曾探大佬的《JavaScript设计模式与开发实践》