JavaScript设计模式——享元模式

享元模式的要求与目标

享元模式要求将对象的属性划分为内部状态与外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量,关于如何划分内部状态和外部状态,下面的几条经验提供了一些指引。
 内部状态存储于对象内部。
 内部状态可以被一些对象共享。
 内部状态独立于具体的场景,通常不会改变。
 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。

享元模式应用举例:文件上传

先假设文件上传方式只有插件和 Flash 这两种。不论是插件上传,还是 Flash 上传,原理都是一样的,当用户选择了文件之后,插件和 Flash 都会通知调用 Window 下的一个全局 JavaScript 函数,它的名字是startUpload,用户选择的文件列表被组合成一个数组 files 塞进该函数的参数列表里。

版本一:没有使用享元模式

代码如下:

var id = 0;
   window.startUpload = function (uploadType, files) { // uploadType 区分是控件还是 flash 
     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><span>文件大小:${this.fileSize}</span> <button class="delFile">删除</button>`;
  this.dom.querySelector('.delFile').onclick = function () {
    that.delFile()
  }
  document.body.appendChild(this.dom)
}

定义一个删除文件的方法,假设该方法中有一个逻辑:当被删除的文件小于 1024 KB 时,该文件将被直接删除。否则页面中会弹出一个提示框,提示用户是否确认要删除该文件,代码如下:

Upload.prototype.delFile = function (fileSize) {
  if (this.fileSize < 1024) {
    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: 100
  },
  {
    fileName: '2.html',
    fileSize: 300
  },
  {
    fileName: '3.txt',
    fileSize: 2000
  }
]);
startUpload('flash', [{
    fileName: '4.txt',
    fileSize: 100
  },
  {
    fileName: '5.html',
    fileSize: 300
  },
  {
    fileName: '6.txt',
    fileSize: 2000
  }
]);

经过验证可以看到当点击删除大小为2000的文件时,弹出了是否确认删除的提示:
在这里插入图片描述

版本二:使用享元模式重构版本一

版本一的文件上传代码里有多少个需要上传的文件,就一共创建了多少个 upload 对象,接下来我们用享元模式重构它。
首先,我们需要确认插件类型 uploadType 是内部状态,那为什么单单 uploadType 是内部状态呢?前面讲过,划分内部状态和外部状态的关键主要有以下几点。
 内部状态储存于对象内部。
 内部状态可以被一些对象共享。
 内部状态独立于具体的场景,通常不会改变。
 外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享。
在文件上传的例子里,upload 对象必须依赖 uploadType 属性才能工作,这是因为插件上传、Flash 上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,必须在对象创建之初就明确它是什么类型的插件,才可以在程序的运行过程中,让它们分别调用各自的 start、pause、cancel、del 等方法。
一旦明确了 uploadType,无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的。而 fileName 和 fileSize 是根据场景而变化的,每个文件的 fileName 和 fileSize 都不一样,
fileName 和 fileSize 没有办法被共享,它们只能被划分为外部状态。

1.剥离外部状态
明确了 uploadType 作为内部状态之后,我们再把其他的外部状态从构造函数中抽离出来,Upload 构造函数中只保留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); // (1)
  if (this.fileSize < 1024) {
    return this.dom.parentNode.removeChild(this.dom);
  }
  if (window.confirm(`确认要删除${this.fileName}吗?`)) {
    return this.dom.parentNode.removeChild(this.dom)
  }
}

在开始删除文件之前,需要读取文件的实际大小,而文件的实际大小被储存在外部管理器uploadManager 中,所以在这里需要通过 uploadManager.setExternalState 方法给共享对象设置正确
的 fileSize,上段代码中的(1)处表示把当前 id 对应的对象的外部状态都组装到共享对象中。
2. 工厂进行对象实例化
接下来定义一个工厂来创建 upload 对象,如果某种内部状态对应的共享对象已经被创建过,那么直接返回这个对象,否则创建一个新的对象:

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

3. 管理器封装外部状态
uploadManager 对象,它负责向 UploadFactory 提交创建对象的请求,并用一个 uploadDatabase 对象保存所有 upload 对象的外部状态,以便在程序运行过程中给upload 共享对象设置外部状态,代码如下:

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}</span><span>文件大小:${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];
        }
      }
    }
  })();

4. 触发上传动作的 startUpload 函数

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: 100
  },
  {
    fileName: '2.html',
    fileSize: 300
  },
  {
    fileName: '3.txt',
    fileSize: 2000
  }
]);
startUpload('flash', [{
    fileName: '4.txt',
    fileSize: 100
  },
  {
    fileName: '5.html',
    fileSize: 300
  },
  {
    fileName: '6.txt',
    fileSize: 2000
  }
]);
总结

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个 factory 对象和一个 manager 对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。
享元模式带来的好处很大程度上取决于如何使用以及何时使用,一般来说,以下情况发生时便可以使用享元模式。
 一个程序中使用了大量的相似对象。
 由于使用了大量对象,造成很大的内存开销。
 对象的大多数状态都可以变为外部状态。
 剥离出对象的外部状态之后,可以用相对较少的共享对象取代大量对象。
可以看到,文件上传的例子完全符合这四点所以很适合用享元模式。
(本文参考于《JavaScript设计模式与开发实战》)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值