• Javascript享元模式


    1 什么是享元模式

    享元(flyweight)模式是一种用于性能优化的模式,“fly”在这里是苍蝇的意思,意为蝇量级。

    享元模式的核心是运用共享技术来有效支持大量细粒度的对象。如果系统中因为创建了大量类似的对象而导致内存占用过高,享元模式就非常有用了。

    假设服装店新到了50套男士衣服和50套女士衣服,为了推销出去,店里决定买一些模特来穿上衣服进行宣传。一般情况下,需要50个男模特和50个女模特,每个模特穿上衣服拍照,实现代码如下:

    var Model = function (sex, underwear) {
      this.sex = sex;
      this.underwear = underwear;
    };
    
    Model.prototype.takePhote = function () {
      console.log("sex=" + this.sex + "underwear=" + this.underwear);
    };
    
    for (let i = 1; i <= 50; i++) {
      var maleModel = new Model("male", "underwear" + i);
      maleModel.takePhote();
    }
    
    for (let j = 1; j <= 50; j++) {
      var femaleModel = new Model("female", "underwear" + j);
      femaleModel.takePhote();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    如果要得到一张照片,每次都需要传入sexunderwear参数,如上所示,现在一共有50套男款服装和50套女款服装,所以一共会产生100个对象。如果之后有10000套衣服,那这个程序可能会因为存在如此多的对象已经提前崩溃。

    其实我们可以想到,虽然有100套衣服,但很显然并不需要50个男模特和50个女模特,男模特和女模特各自有一个就足够了,他们可以分别穿上不同的衣服来拍照。现在我们根据以上逻辑改写一下代码:

    var Model = function (sex) {
      this.sex = sex;
    };
    
    Model.prototype.takePhote = function () {
      console.log("sex=" + this.sex + ", underwear=" + this.underwear);
    };
    
    // 首先分别创建一个男模特和一个女模特
    var maleModel = new Model("male");
    var femaleModel = new Model("female");
    
    // 依次让男模特穿上所有的男装拍照
    for (let i = 1; i <= 50; i++) {
      maleModel.underwear = "underwear" + i;
      maleModel.takePhote();
    }
    
    // 依次让女模特穿上所有的女装拍照
    for (let j = 1; j <= 50; j++) {
      femaleModel.underwear = "underwear" + j;
      femaleModel.takePhote();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    我们可以看到,改进之后的代码,只有两个对象,就可以完成同样的拍照的任务。

    2 内部状态与外部状态

    上面的这个例子便是享元模式的雏形,享元模式要求将对象的属性划分为内部状态外部状态(状态在这里通常指属性)。享元模式的目标是尽量减少共享对象的数量

    那么如何区分内部状态和外部状态呢,我们可以根据下面这几条特征来区分:

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

    这样一来,我们便可以把所有内部状态相同的对象都指定为同一个共享的对象。而外部状态可以从对象身上剥离出来,并储存在外部。

    剥离了外部状态的对象成为共享对象,外部状态在必要时被传入共享对象来组装成一个完整的对象。虽然组装外部状态成为一个完整对象的过程需要花费一定的时间,但却可以大大减少系统中的对象数量。因此,享元模式是一种用时间换空间的优化模式。

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

    3 享元模式的通用结构

    上面展示的例子还不是一个完整的享元模式,在这个例子中还存在以下两个问题:

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

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

    4 文件上传

    4.1 对象爆炸

    什么是对象爆炸呢,比如说在文件上传功能中,可以选择依照队列一个一个地排队上传,也可以同时选择2000个文件。每一个文件都对应着一个JavaScript上传对象的创建,那么同时上传2000个文件,程序中就需要同时new2000个upload对象,这对浏览器会造成很大的冲击。

    比如我们要实现使用插件或者Flash上传文件的功能,当用户选择了需要上传的文件之后,它们会去通知调用Window下的一个全局Javascript函数startUpload,用户选择的文件列表被组合成一个数组files塞进该函数的参数列表种,代码如下:

    var id = 0;
    
    window.startUpload = function (uploadType, files) {
      for (let i = 0, file; (file = files[i++]); ) {
        var uploadObj = new upload(uploadType, file.fileName, file.fileSize);
        uploadObj.init(id++); // 为upload对象设置一个唯一的id
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当用户选择完文件之后,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 =
        "文件名称:" +
        this.fileName +
        ", 文件大小: " +
        this.fileSize +
        "" +
        '';
      this.dom.querySelector(".delFile").onclick = function () {
        that.delFile();
      };
      document.body.appendChild(this.dom);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    假设upload对象只有删除文件的功能,对应的方法是Upload.prototype.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);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    接下来分别创建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 },
    ]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    4.2 享元模式重构

    首先确认内部状态和外部状态,在上面的例子中,只有上传类型uploadType是内部状态,在文件上传的例子里,upload对象必须依赖uploadType属性才能工作,这是因为插件上传、Flash上传、表单上传的实际工作原理有很大的区别,它们各自调用的接口也是完全不一样的,必须在对象创建之初就明确它是什么类型的插件,才可以在程序的运行过程中,让它们分别调用各自的方法。

    无论我们使用什么方式上传,这个上传对象都是可以被任何文件共用的。而fileNamefileSize是根据场景而变化的,每个文件的fileNamefileSize都不一样, 它们只能被划分为外部状态。

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

    var Upload = function (uploadType) {
      this.uploadType = uploadType;
    };
    
    • 1
    • 2
    • 3

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

    // 剥离外部状态
    Upload.prototype.delFile = function (id) {
      uploadManager.setExternalState(id, this); // 将id对应的对象的外部状态组装到共享对象中
      if (this.fileSize < 3000) {
        return this.dom.parentNode.removeChild(this.dom);
      }
      if (window.confirm("确定要删除该文件吗? " + this.fileName)) {
        return this.dom.parentNode.removeChild(this.dom);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    // 使用工厂模式进行对象实例化
    var UploadFactory = (function () {
      var createdFlyWeightObjs = {};
      return {
        create: function (uploadType) {
          if (createdFlyWeightObjs[uploadType]) {
            return createdFlyWeightObjs[uploadType];
          }
          return (createdFlyWeightObjs[uploadType] = new Upload(uploadType));
        },
      };
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现在我们来完善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 =
            "文件名称:" +
            fileName +
            ", 文件大小: " +
            fileSize +
            "" +
            '';
          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];
          }
        },
      };
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    触发上传动作:

    var id = 0;
    window.startUpload = function (uploadType, files) {
      for (var i = 0, file; (file = files[i++]); ) {
        uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
      }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    测试一下:

    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 },
    ]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    享元模式重构之前的代码里一共创建了6个upload对象,而通过享元模式重构之后,对象的数量减少为2,更幸运的是, 就算现在同时上传2000个文件,需要创建的upload对象数量依然是2。

    5 没有内部状态的享元模式

    在文件上传的例子中,我们分别进行过插件调用和Flash调用,导致程序中创建了内部状态不同的两个共享对象。但是在文件上传程序里,一般都会提前通过特性检测来选择一种上传方式,如果浏览器支持插件就用插件上传,如果不支持插件,就用Flash上传。

    那么这种情况下,之前作为内部状态存在的uploadType属性是可以删掉的,在继续使用享元模式的前提下,构造函数Upload就变成了无参数的形式:

    var Upload = function () {};
    
    • 1

    其他属性如依然可以作为外部状态保存在共享对象外部,改写创建享元对象的工厂,代码如下:

    // 使用工厂模式进行对象实例化
    var UploadFactory = (function () {
      var uploadObj;
      return {
        create: function () {
          if (uploadObj) {
            return uploadObj;
          }
          return (uploadObj = new Upload());
        },
      };
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    管理器部分的代码不需要改动,还是负责剥离和组装外部状态。可以看到,当对象没有内部状态的时候,生产共享对象的工厂实际上变成了一个单例工厂。虽然这时候的共享对象没有内部状态的区分,但还是有剥离外部状态的过程,我们依然倾向于称之为享元模式。

    6 对象池

    对象池维护一个装载空闲对象的池子,如果需要对象的时候,不是直接new,而是转从对象池里获取。如果对象池里没有空闲对象,则创建一个新的对象,当获取出的对象完成它的职责之后, 再进入
    池子等待被下次获取。

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

    比如说,在地图软件中,经常会出现一些标志地名的小气泡,如下所示:
    请添加图片描述
    当我搜索故宫时,出现了3个小气泡,当我搜索王府井时,出现了5个气泡,按照对象池的思想,在第二次搜索开始之前,并不会把第一次创建的3个小气泡删除掉,而是把它们放进对象池。这样在第二次的搜索结果页面里,我们只需要再创建2个小气泡而不是5个。
    请添加图片描述
    先定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包里,这个工厂有两个暴露对外的方法,create表示获取一个div节点,recover表示回收一个div节点:

    var toolTipFactory = (function () {
      var toolTipPool = []; // toolTip 对象池
      return {
        create: function () {
          // 如果对象池为空
          if (toolTipPool.length === 0) {
            var div = document.createElement("div"); // 创建一个 dom
            document.body.appendChild(div);
            return div;
          } else {
            // 如果对象池里不为空
            return toolTipPool.shift(); // 则从对象池中取出一个 dom
          }
        },
        recover: function (tooltipDom) {
          return toolTipPool.push(tooltipDom); // 对象池回收 dom
        },
      };
    })();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    第一次搜索时,需要创建3个小气泡节点,为了方便回收,用一个数组ary记录它们:

    var ary = [];
    for (var i = 0, str; (str = ["A", "B", "C"][i++]); ) {
      var toolTip = toolTipFactory.create();
      toolTip.innerHTML = str;
      ary.push(toolTip);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    接下来假设地图需要开始重新绘制,在此之前要把这3个节点回收进对象池:

    for (var i = 0, toolTip; (toolTip = ary[i++]); ) {
      toolTipFactory.recover(toolTip);
    }
    
    • 1
    • 2
    • 3

    再创建5个小气泡:

    for (var i = 0, str; (str = ["A", "B", "C", "D", "E"][i++]); ) {
      var toolTip = toolTipFactory.create();
      toolTip.innerHTML = str;
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    现在再测试一番,页面中出现了5个节点,上一次创建好的节点被共享给了下一次操作。对象池跟享元模式的思想有点相似,虽然innerHTML的值也可以看成节点的外部状态,但在这里我们并没有主动分离内部状态和外部状态的过程。

    7 通用对象池实现

    我们还可以在对象池工厂里,把创建对象的具体过程封装起来,实现一个通用的对象池:

    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);
        },
      };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    现在利用objectPoolFactory来创建一个装载一些iframe的对象池:

    var iframeFactory = objectPoolFactory(function () {
      var iframe = document.createElement("iframe");
      document.body.appendChild(iframe);
      iframe.onload = function () {
        iframe.onload = null; // 防止 iframe 重复加载
        iframeFactory.recover(iframe); // iframe 加载完成之后回收节点
      };
      return iframe;
    });
    
    var iframe1 = iframeFactory.create();
    iframe1.src = "http:// baidu.com";
    
    var iframe2 = iframeFactory.create();
    iframe2.src = "http:// QQ.com";
    
    setTimeout(function () {
      var iframe3 = iframeFactory.create();
      iframe3.src = "http:// 163.com";
    }, 3000);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    不知道吧?未加工的食物可以帮助你减肥
    S0003-Mac下iTerm2+zsh+ohmyzsh打造优雅美观终端
    记录一个在写项目中遇到的Maven依赖无法导入的问题
    系统设计.短链系统设计
    Optional——优雅判空
    电子信息工程专业课复习知识点总结:(四)信号与系统、数字信号处理
    【Keil】编译选项设置 Warning 为 error
    如何公网远程访问OpenWRT软路由web界面
    《变形监测与数据处理》笔记/期末复习资料(择期补充更新)
    Spring Boot Actuator 模块,spring-boot-starter-actuator
  • 原文地址:https://blog.csdn.net/m0_46612221/article/details/134228869