Javascript享元模式

Javascript享元模式

  • [1 什么是享元模式](#1 什么是享元模式)
  • [2 内部状态与外部状态](#2 内部状态与外部状态)
  • [3 享元模式的通用结构](#3 享元模式的通用结构)
  • [4 文件上传](#4 文件上传)
    • [4.1 对象爆炸](#4.1 对象爆炸)
    • [4.2 享元模式重构](#4.2 享元模式重构)
  • [5 没有内部状态的享元模式](#5 没有内部状态的享元模式)
  • [6 对象池](#6 对象池)
  • [7 通用对象池实现](#7 通用对象池实现)

1 什么是享元模式

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

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

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

javascript 复制代码
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();
}

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

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

javascript 复制代码
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();
}

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

2 内部状态与外部状态

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

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

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

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

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

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

3 享元模式的通用结构

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

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

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

4 文件上传

4.1 对象爆炸

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

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

javascript 复制代码
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
  }
};

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

javascript 复制代码
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 +
    ", 文件大小: " +
    this.fileSize +
    "</span>" +
    '<button class="delFile">删除</button>';
  this.dom.querySelector(".delFile").onclick = function () {
    that.delFile();
  };
  document.body.appendChild(this.dom);
};

假设upload对象只有删除文件的功能,对应的方法是Upload.prototype.delFile。该方法中有一个逻辑:当被删除的文件小于3000KB时,该文件将被直接删除,否则页面中会弹出一个提示框,提示用户是否确认要删除该文件,代码如下:

javascript 复制代码
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上传对象:

javascript 复制代码
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 },
]);

4.2 享元模式重构

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

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

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

javascript 复制代码
var Upload = function (uploadType) {
  this.uploadType = uploadType;
};

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

javascript 复制代码
// 剥离外部状态
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);
  }
};

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

javascript 复制代码
// 使用工厂模式进行对象实例化
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共享对象设置外部状态,代码如下:

javascript 复制代码
// 使用管理器封装外部状态
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 +
        ", 文件大小: " +
        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];
      }
    },
  };
})();

触发上传动作:

javascript 复制代码
var id = 0;
window.startUpload = function (uploadType, files) {
  for (var i = 0, file; (file = files[i++]); ) {
    uploadManager.add(++id, uploadType, file.fileName, file.fileSize);
  }
};

测试一下:

javascript 复制代码
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 },
]);

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

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

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

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

javascript 复制代码
var Upload = function () {};

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

javascript 复制代码
// 使用工厂模式进行对象实例化
var UploadFactory = (function () {
  var uploadObj;
  return {
    create: function () {
      if (uploadObj) {
        return uploadObj;
      }
      return (uploadObj = new Upload());
    },
  };
})();

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

6 对象池

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

池子等待被下次获取。

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

比如说,在地图软件中,经常会出现一些标志地名的小气泡,如下所示:

当我搜索故宫时,出现了3个小气泡,当我搜索王府井时,出现了5个气泡,按照对象池的思想,在第二次搜索开始之前,并不会把第一次创建的3个小气泡删除掉,而是把它们放进对象池。这样在第二次的搜索结果页面里,我们只需要再创建2个小气泡而不是5个。

先定义一个获取小气泡节点的工厂,作为对象池的数组成为私有属性被包含在工厂闭包里,这个工厂有两个暴露对外的方法,create表示获取一个div节点,recover表示回收一个div节点:

javascript 复制代码
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
    },
  };
})();

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

javascript 复制代码
var ary = [];
for (var i = 0, str; (str = ["A", "B", "C"][i++]); ) {
  var toolTip = toolTipFactory.create();
  toolTip.innerHTML = str;
  ary.push(toolTip);
}

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

javascript 复制代码
for (var i = 0, toolTip; (toolTip = ary[i++]); ) {
  toolTipFactory.recover(toolTip);
}

再创建5个小气泡:

javascript 复制代码
for (var i = 0, str; (str = ["A", "B", "C", "D", "E"][i++]); ) {
  var toolTip = toolTipFactory.create();
  toolTip.innerHTML = str;
}

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

7 通用对象池实现

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

javascript 复制代码
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);
    },
  };
};

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

javascript 复制代码
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);
相关推荐
susu10830189113 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
湫ccc27 分钟前
《Python基础》之字符串格式化输出
开发语言·python
mqiqe1 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin1 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
Ysjt | 深2 小时前
C++多线程编程入门教程(优质版)
java·开发语言·jvm·c++
ephemerals__2 小时前
【c++丨STL】list模拟实现(附源码)
开发语言·c++·list
码农飞飞2 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货2 小时前
Rust 的简介
开发语言·后端·rust