享元模式解读(1)

154 阅读5分钟
原文链接: damobing.com

前言

本文是基于《javascript设计模式与开发实践》的享元模式相关章节整理实践而出,建议阅读时间为15-25min.

概念解读

享元模式(flyweight)是一种用于性能优化的模式,其核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量的对象而导致内存占用过高,享元模式就非常有用了。在js中,浏览器特别是移动端的浏览器分配的内存并不算多,如何节省内存是一件非常有意义的事情。

初步了解

假设我们工厂生产50种男装,那么需要模特来试穿拍照,其代码可能是如下的:

var model = function(sex){
 this.sex = sex
}
model.prototype.takePhoto = function(){
 console.log(this.sex+" is wearing "+this.underwear) 
}

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

从上面的代码分析可以看出在 试穿50个男装的时候,新建了50个对象,如果是1000件男装,可能需要1000个对象,这是非常占用内存的。在实际生活中,我们知道实际一般的服装店都是固定一个模特来完成试装的,而享元模式也是这样设计的。所以我们对代码进行优化下:这样就可以实现用一个模特来完成试装。

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

内部状态与外部状态

上面的例子是享元模式的雏形,实际的享元模式属性划分为内部状态与外部状态(状态这里通常指属性)。关于如何划分内部与外部状态可以参考以下的几条建议 :

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

根据以上的划分建议,我们可以把共享的状态指定为一个共享的对象,而外部状态从对象剥离,并存储在外部。同时,在需要的时候也是把外部状态通过传入共享对象来完成组装。所以可以这样通俗的理解,它的成本是时间换空间,多了传入外部状态的时间,少了占用内存的空间。

通用结构

在上述的例子中还存在着两个问题你发现了么?

  • 通过构造函数创建了对象,但实际,我们可能并不是开始就需要创建对象
  • 在例子中,我们是手动设置的underwear属性,但实际情况可能比这个复杂的多。

对于第一个问题,可以使用对象工程模式来解决,只有当共享对象真正需要的时候,才去从工厂中创建出来;对于第二个问题,可以用一个管理器来记录对象相关的外部状态,从而使得外部状态通过某个钩子与共享对象联系起来。

文件上传

以下将重点讲述一个文件上传的案例处理细节,以及为什么这样做。

备注:此书的作者是腾讯合金团队的高级前端工程师曾探所著,所以很多例子会非常dry,并且在大厂的代码中有所使用,拿来举例是极好的。

对象爆炸

在微云的(微云是什么就不用科普了吧)上传模块中,会遇到一个对象爆炸的问题。微云的上传功能可以按照队列进行依次上传,但也同时支持选择200个文件。每个文件都对应着一个对象的创建,在作者开发的第一个版本中创建了200个对象,直接导致了浏览器的卡死。下面从代码角度去分析这个过程。

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.prototype.init = function(id){
 var that = this ;
 this.id = id;
 this.dom = doocument.createElement("div') ;
 this.dom.innerHtml = `<span>文件名称:${this.fileName},文件大小:${this.fileSize}</span><button class="delfile">删除</button>` ;
 this.dom.querySelector(".delfile").onClick = function(){
  that.delFile();//注意 点击事件中的this不是当前对象的this,所以开始 需要赋值给that
 }
 document.body.appendChild(thi)
}

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

}

在创建新文件对象的时候,会依次创建三个插件或者flash的上传对象

startUpload("plugin",[{
fileName:'1.txt',
fileSize:1000
},
{
fileName:'2.html',
fileSize:1000
}
{
fileName:'3.png',
fileSize:1000
}])

享元模式重构

进行状态拆分,其中uploadType属于内部状态,而fileName和fielSize属于外部状态,相信你觉得上传类型为什么是内部状态呢?主要是因为不同的上传类型其内部实现是不一样的,各自调用的接口也不同。

剥离外部状态

所以在Upload的构造函数中保留upload状态,其他的就不需要了。而init函数也不需要 了,因为外部状态会放在外面的管理器中。

var Upload = function (uploadType){

 this.uploadType = uploadType ;
}

Upload.prototype.delFile = function(id){
    uploadManger.setExternState(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)
  }
}

未完待续

在下一篇文章会为大家介绍(工厂进行对象实例化、管理器封装外部状态)、 享元模式的适用性、再谈外部和内部状态、对象池。