前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >享元模式

享元模式

作者头像
一粒小麦
发布2019-11-19 17:52:02
4110
发布2019-11-19 17:52:02
举报
文章被收录于专栏:一Li小麦一Li小麦

享元模式

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

一个内衣工厂,有50个男性内衣产品和50个女性内衣产品。现在需要请一些模特穿上内衣来拍详情。通常应该怎么做?如何用代码描述这个过程?

你可能这么写:

代码语言:javascript
复制
class Model{
  constructor({sex,underwear}){
    this.sex=sex;
    this.underwear=underwear;
  }

  takePhoto(){
    console.log(this.sex,this.underwear)
  }
}

for(let i=0;i<50;i++){
  const model=new Model({sex:'male',underwear:`underwaer-${i}`});
  model.takePhoto();
}

for(let i=0;i<50;i++){
  const model=new Model({sex:'female',underwear:`underwaer-${i}`});
  model.takePhoto();
}

如果按照此算法,100个产品将new出100个model对象,如果增加到1w,10w个,很快就会崩溃。

实际上,男女模特一个就够了。

代码语言:javascript
复制
class Model{
  constructor(sex){
    this.sex=sex;
  }

  takePhoto(){
    console.log(this.sex,this.underwear)
  }
}


const maleModel=new Model('male');
const femaleModel=new Model('female');
for(let i=0;i<50;i++){
  maleModel.underwear=`underwaer-${i}`;
  maleModel.takePhoto();
}

for(let i=0;i<50;i++){
  femaleModel.underwear=`underwaer-${i}`;
  femaleModel.takePhoto();
}

上面的代码很形象地描述了"穿内衣"这个过程,事实就是这样,请一男一女两个模特,然后就可以拍照了。

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

在上面的例子中,性别是内部状态,内衣是外部状态,通过区分这两种状态,大大减少了系统中的对象数量。通常来讲,内部状态有多少种组合,系统中便最多存在多少个对象,因为性别通常只有男女两种,所以该内衣厂商最多只需要2个对象。

通用结构

上述的代码还存在问题:

  • 我们通过构造函数显式new出了男女两个model对象,在其他系统中,也许并不是一开始就需要所有的共享对象。
  • 给model对象手动设置了underwear外部状态,不是一个最好的方式,因为外部状态可能会相当复杂,它们与共享对象的联系会变得困难。

第一个问题可以通过对象工厂来解决,只有当某种共享对象被真正需要时,它才从工厂中被创建出来。对于第二个问题,可以用一个管理器来记录对象相关的外部状态,使这些外部状态通过某个钩子和共享对象联系起来。

听起来很扯,现在就以文件上传为例。

文件上传

下面看一个实际工作中遇到的问题。(重点)

项目背景:对象爆炸

一个文件上传的功能,要求支持2000个文件。原理如下:上传分为不同的模式(uploadType)比如flash上传,表单上传等。当用户选择了文件之后,控件都会通知调用一个全局startUploadt函数,用户选择的文件列表被组合成一个数组files塞进该函数的参数列表里:

代码语言:javascript
复制
let id=0;

const startUpload=(uploadType,files)=>{
  files.forEach((file)=>{
    const {fileName,fileSize}=file;
    const uploadObj=new Upload(uploadType,{fileName,fileSize});
    uploadObj.init(id++);//设置id
  })
}

其中Upload对象是这么写的:

代码语言:javascript
复制
class Upload{
  constructor(uploadType,{fileName,fileSize}){
    this.uploadType=uploadType;
    this.fileName=fileName;
    this.fileSize=fileSize;

    this.dom=null;
  }

  init(id){
    this.id=id;
    this.dom=document.createElement('div');
    this.dom.innerHTML=
    `<span>文件名:${this.fileName},文件大小:${this.fileSize}</span>
     <a id="del${id}" style="color:red;" href="javascript:void(0)">删除</a>
    `;

    document.querySelector(`del${id}`).addEventListener('click',(e)=>{
      this.delFile(id);
    })

    document.body.appendChild(this.dom)
  }

  delFile(id){
    //删除业务逻辑,文件小于3000k,直接删除,否则confirm提示。
    if(this.fileSize>3000){
      if(window.confirm('确定删除嘛?')){
        this.dom.parentNode.removeChild(this.dom);
      }
    }else{
      this.dom.parentNode.removeChild(this.dom);
    }
  }
}

假设每个文件都需要一个生成一个upload对象,在数据量到达2000个的时候,直接假死。

尝试用享元模式重构它。

首先要区分外部状态和内部状态,一般来说内部状态和外部状态包括以下要点:

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

显然符合条件的内部状态只有 uploadType。那么fileName和fileSize都可以不作为Upload的内置参数了。正如穿衣这个动作,设置对象外部状态也可以通过一个管理器实现。

代码语言:javascript
复制
class Upload{
  constructor(uploadType){
    this.uploadType=uploadType;
    this.dom=null;
  }

  delFile(id){
    // 此处应有一条语句设置外部状态到this上拿到fileName和fileSize并绑定到this
    // ...
    if(this.fileSize>3000){
      if(window.confirm('确定删除嘛?')){
        this.dom.parentNode.removeChild(this.dom);
      }
    }else{
      this.dom.parentNode.removeChild(this.dom);
    }
  }
  // ...
}

其次,实现一个工厂函数来创建Upload对象。这里很像是一个单例的创建。

代码语言:javascript
复制
const upLoadFactory=(()=>{
  let createdFlyWeightObjs={};
  return {
    create:function(uploadType){
      if(createdFlyWeightObjs[uploadType]){
        return createdFlyWeightObjs[uploadType];
      }
      createdFlyWeightObjs[uploadType]=new Upload(uploadType);
      return createdFlyWeightObjs[uploadType];
    }
  }
})();

接下来就是设置一个upload管理器(uploadManager),期望实现以下功能:

  • 完成原有对象init的主要工作,向工厂函数提交创建upload对象的请求
  • 用一个uploadDatabase对象保存所有upload对象的外部状态(fileName和fileSize),以便在程序运行过程中给upload共享对象设置外部状态,
代码语言:javascript
复制
const uploadManager = (() => {
  // 共享池
  let uploadDataBase = {};
  return {
    add: function (id, uploadType, fileName, fileSize) {
      const filyWeightObj = upLoadFactory.create(uploadType);
      const dom = document.createElement('div');
      dom.innerHTML =
        `<span>文件名:${fileName},文件大小:${fileSize}</span>
        <a id="del${id}" style="color:red;" href="javascript:void(0)">删除</a>
      `;

      document.querySelector(`del${id}`).addEventListener('click', (e) => {
        filyWeightObj.delFile(id);
      })

      document.body.appendChild(dom);
      uploadDataBase[id]={uploadType, fileName, fileSize};

      return filyWeightObj;
    },

        // 期望使用uploadManager.setExternalState(id,this)来绑定到this上
    setExternalState:function(id,filyWeightObj){
      let uploadData=uploadDataBase[id];
      Object.keys(uploadData).forEach((key)=>{
        filyWeightObj[key]=uploadData[key];
      })
    }
  }
})();

add方法拿到upload对象,完成业务逻辑(因为只执行一次),再把上传信息放到状态共享池中。而 setExternalState通过id拿到共享池对应的状态,绑定到upload对象的this上。

代码语言:javascript
复制
uploadManager.setExternalState(id,this)

那么,调用方式不再是循环内new一个Upload,而是

代码语言:javascript
复制
const startUpload = (uploadType, files) => {
  files.forEach((file) => {
    const { fileName, fileSize } = file;
    cuploadManager.add(++id,uploadType,file.fileName,file.fileSize);
  })
}

最后无论你有几个上传类型,就new几个对象,而不再是一个循环new一个上传对象,大大减少了内存。

适用场景

享元模式是一种很好的性能优化方案,但它也会带来一些复杂性的问题,从前面两组代码的比较可以看到,使用了享元模式之后,我们需要分别多维护一个factory对象和一个manager对象,在大部分不必要使用享元模式的环境下,这些开销是可以避免的。

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

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

使用小结

我们可以尝试提炼享元模式的实质:实现享元模式的关键是把内部状态和外部状态分离开来,它的实现过程就是剥离外部状态。有多少种内部状态的组合,系统中便最多存在多少个共享对象,而外部状态储存在其他地方(共享对象的外部),在必要时被传入共享对象来组装成一个完整的对象。

享元模式和单例模式有什么区别?设想我们去掉内部状态:

代码语言:javascript
复制
const upLoadFactory=(()=>{
  return {
    create:function(){
      if(upload){
        return upload;
      }
      upload=new Upload();
      return upload;
    }
  }
})();

这里就完成变成了单例工厂。在这种场景下习惯上仍然被称为享元模式。

但是如果去掉外部状态,你的对象池就有可能会变得异常庞大,那就不是享元模式了。

共享对象池

有内部状态的情况下,对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,就往里面加一个。

图书馆就是一个很好的例子,一个班级50人,人手一套《十万个为什么》显然是不划算的。书架上有这套书,借就是了。

对象池技术的应用非常广泛,HTTP连接池和数据库连接池都是其代表应用。在Web前端开发中,对象池使用最多的场景大概就是跟DOM有关的操作。很多空间和时间都消耗在了DOM节点上,如何避免频繁地创建和删除DOM节点就成了一个有意义的话题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 享元模式
    • 通用结构
      • 文件上传
        • 项目背景:对象爆炸
      • 适用场景
        • 使用小结
          • 共享对象池
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档