享元模式

享元模式(Flyweight Pattern)是一种结构型设计模式,主要用于优化对象的复用,以减少创建大量类似对象所带来的开销。这种模式的核心思想是将一个对象的内部状态和外部状态区分开,内部状态是对象共享的部分,外部状态是对象独立的部分。

享元模式通常被应用于以下的应用场景:

  1. 系统中存在大量相似或完全相同的对象。
  2. 对象的创建和存储成本高。
  3. 对象可以轻易地被共享,而不影响其外部行为。

了解享元模式

假设有一家衣服工厂,目前的产品有 50 种男式上衣和 50 种女士上衣,为了推销产品,工厂决定生产一些塑料模特来穿上他们的内衣拍成广告照片。 正常情况下需要 50 个男模特和 50 个女模特,然后让他们每人分别穿上一件上衣来拍照。不使用享元模式的情况下,在程序里也许会这样写

js 复制代码
const Model = function (sex, underwear) {
  this.sex = sex;
  this.underwear = underwear;
};
Model.prototype.takePhoto = function () {
  console.log("sex= " + this.sex + " underwear=" + this.underwear);
};
for (let i = 1; i <= 50; i++) {
  const maleModel = new Model("male", "underwear" + i);
  maleModel.takePhoto();
}
for (var j = 1; j <= 50; j++) {
  const femaleModel = new Model("female", "underwear" + j);
  femaleModel.takePhoto();
}

要得到一张照片,每次都需要传入 sex 和 underwear 参数,如上所述,现在一共有 50 种男上衣和 50 种女上衣,所以一共会产生 100 个对象。如果将来生产了 10000 种上衣,那这个程序可能会因为存在如此多的对象已经提前崩溃。

为了减少对象的数量,我们可以使用享元模式。在这个场景中,性别是对象的内部状态,因为每种性别只需要一个模特。而上衣是外部状 态,因为它随时间和情境而变化。我们可以为每个性别只创建一个模特对象,并为这些对象动态设置上衣来拍照。

现在来改写一下代码,既然只需要区别男女模特,那我们先把 underwear 参数从构造函数中移除,构造函数只接收 sex 参数:

js 复制代码
class FlyweightModel {
  constructor(sex) {
    this.sex = sex;
  }

  takePhoto(underwear) {
    console.log("sex= " + this.sex + " underwear=" + underwear);
  }
}

// 享元工厂
const modelFactory = (function () {
  const modelPool = {};

  return {
    createModel: function (sex) {
      if (!modelPool[sex]) {
        modelPool[sex] = new FlyweightModel(sex);
      }
      return modelPool[sex];
    },
  };
})();

// 使用享元工厂创建模特对象
const maleModel = modelFactory.createModel("male");
const femaleModel = modelFactory.createModel("female");

// 使用模特对象拍照
for (let i = 1; i <= 50; i++) {
  maleModel.takePhoto("underwear" + i);
}

for (let j = 1; j <= 50; j++) {
  femaleModel.takePhoto("underwear" + j);
}

通过这种方式,无论有多少种上衣,我们只需要为每个性别创建一个模特对象,从而大大减少了对象的数量。

文本编辑器中的字符样式

考虑一个文本编辑器,文本中可以有成千上万的字符,每个字符可能有其特定的样式,如加粗、斜体、下划线、字体大小和颜色等。如果为每个字符都存储这些样式信息,将会浪费大量的内存。

如果不使用享元模式,每个字符都有自己的样式信息,如下代码所示:

js 复制代码
class Character {
  constructor(char, bold, italic, underline, color, fontSize) {
    this.char = char;
    this.bold = bold;
    this.italic = italic;
    this.underline = underline;
    this.color = color;
    this.fontSize = fontSize;
  }

  display() {
    console.log(this.char);
  }
}

const charA = new Character("A", true, false, true, "red", 16);
const charB = new Character("B", true, false, true, "red", 16);

charA.display();
charB.display();

在上面的代码中,尽管 charA 和 charB 具有相同的样式,但每个字符都存储了这些样式信息。

接下来我们看看使用享元模式的情况是怎么样的:

js 复制代码
// 内部状态:样式
class TextStyle {
  constructor(bold, italic, underline, color, fontSize) {
    this.bold = bold;
    this.italic = italic;
    this.underline = underline;
    this.color = color;
    this.fontSize = fontSize;
  }
}

// 外部状态:字符
class Character {
  constructor(char, style) {
    this.char = char;
    this.style = style;
  }

  display() {
    console.log(this.char);
  }
}

// 享元工厂
class StyleFactory {
  constructor() {
    this.styleMap = {};
  }

  getStyle(bold, italic, underline, color, fontSize) {
    const key = `${bold}-${italic}-${underline}-${color}-${fontSize}`;
    if (!this.styleMap[key]) {
      this.styleMap[key] = new TextStyle(
        bold,
        italic,
        underline,
        color,
        fontSize
      );
    }
    return this.styleMap[key];
  }
}

const styleFactory = new StyleFactory();
const sharedStyle = styleFactory.getStyle(true, false, true, "red", 16);

const charA = new Character("A", sharedStyle);
const charB = new Character("B", sharedStyle);

在上面的这段代码中,定义了一个文本编辑器的简化模型,其中 TextStyle 类描述了文本的样式,Character 类表示带有特定样式的字符。为了避免创建重复的样式对象,我们使用 StyleFactory 工厂来确保相同的样式被共享和复用,从而优化内存使用。

如果遇到样式不同的情况下享元模式仍然可以发挥作用。关键在于即使样式有所不同,很多样式仍然可能是相同的或者重复的。所以,对于这些重复的样式,享元模式能够确保我们只为它们创建一个对象。

js 复制代码
const styleFactory = new StyleFactory();

const redBoldStyle = styleFactory.getStyle(true, false, false, "red", 16);
const blueItalicStyle = styleFactory.getStyle(false, true, false, "blue", 16);

const charA = new Character("A", redBoldStyle);
const charB = new Character("B", redBoldStyle);
const charC = new Character("C", blueItalicStyle);

即使存在多种不同的样式,只要有一部分样式是重复的,享元模式都可以帮助我们节省内存。在实际的应用中,可能有数千甚至数百万个字符,但样式的种类可能远远少于字符数量。因此,通过共享样式对象,我们可以显著减少内存使用。

享元模式的通用结构

享元模式的核心思想是共享对象,从而减少系统中的对象数量并减少内存使用。这种模式在面对大量相似对象时特别有用。以下是享元模式的通用结构:

  1. 享元: 这是所有具体享元类的父类或接口,它定义了一个接口来接收和获取外部状态。在我们前面的示例中,TextStyle 就是一个具体的享元。
  2. 具体享元:实现享元接口并存储内部状态。这个状态必须是共享的和不可变的。多个外部状态可以共享相同的内部状态。
  3. 享元工厂:负责创建和管理享元对象。它确保相同的内部状态对应的对象是共享的。如果请求的对象已经存在,它就返回这个对象;否则,它创建一个新的对象。在前面的示例中,StyleFactory 就是享元工厂。
  4. 客户端:客户端维护对所有享元的引用,并计算或存储享元的外部状态。客户端代码通常会创建一组享元并在它们之间共享状态
  5. 外部状态:这些状态是依赖于具体的应用场景并由客户端存储或计算的。因为它们不是共享的,所以它们应该独立于享元对象。在我们的示例中,Character 类的 char 属性是外部状态。
  6. 内部状态:这些状态是在享元对象内部存储的,并且是共享的。它们独立于具体的场景并且不会随着场景的变化而改变。

享元模式通过分离内部和外部状态,使得系统可以更有效地共享对象,从而减少对象的数量和内存使用。

享元模式在 React 中的应用

让我们考虑一个在 React 中遇到的一个场景:一个网页上有许多工具提示 message。当用户将鼠标悬停在某些元素上时,工具提示会显示相关的信息。这些工具提示都具有相似的样式和动画,但显示的文本内容不同。

在没有使用享元模式的情况下,每当一个元素需要显示工具提示时,可能会为其创建一个新的 DOM 元素。但如果页面上有数百个这样的元素,这样做将会很低效。

使用享元模式的思想,我们可以只创建一个工具提示的 DOM 元素,并在需要时重用它,只是根据需要改变其内容和位置。

接下来情况具体代码实现:

jsx 复制代码
const TooltipContext = React.createContext();

function TooltipProvider({ children }) {
  const [tooltipData, setTooltipData] = React.useState(null);

  const showTooltip = (content, position) => {
    setTooltipData({ content, position });
  };

  const hideTooltip = () => {
    setTooltipData(null);
  };

  return (
    <TooltipContext.Provider value={showTooltip}>
      {children}
      {tooltipData && (
        <Tooltip
          content={tooltipData.content}
          position={tooltipData.position}
        />
      )}
    </TooltipContext.Provider>
  );
}

function Tooltip({ content, position }) {
  const styles = {
    position: "absolute",
    top: position.top,
    left: position.left,
    /* other styles */
  };

  return <div style={styles}>{content}</div>;
}

function HoverableElement({ content }) {
  const showTooltip = React.useContext(TooltipContext);

  const handleMouseOver = (e) => {
    const position = { top: e.clientY, left: e.clientX };
    showTooltip(content, position);
  };

  return <div onMouseOver={handleMouseOver}>Hover me!</div>;
}

// 使用:
// <TooltipProvider>
//   <HoverableElement content="Tooltip content here" />
//   {/* ... other HoverableElements */}
// </TooltipProvider>

在上面的示例中,TooltipProvider 组件管理单个 Tooltip 的状态和位置。HoverableElement 组件在悬停时使用上下文来调用 showTooltip 函数,该函数会更新 Tooltip 的内容和位置。这样,无论页面上有多少 HoverableElement 组件,都只有一个 Tooltip DOM 元素,这就是享元模式的应用。

参考文献

  • 书籍:JavaScript 设计模式与开发实践

总结

享元模式它通过将对象的共享部分(内部状态)与非共享部分(外部状态)分离,使得对象能够在多个上下文中共享。这种方法可以大大减少系统中对象的数量,从而减少内存占用和提高性能,尤其在处理大量类似对象时尤为有效。

最后分享两个我的两个开源项目,它们分别是:

这两个项目都会一直维护的,如果你也喜欢,欢迎 star 🚗🚗🚗

相关推荐
博客zhu虎康10 分钟前
ElementUI 的 form 表单校验
前端·javascript·elementui
敲啊敲952739 分钟前
5.npm包
前端·npm·node.js
CodeClimb1 小时前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
咸鱼翻面儿1 小时前
Javascript异步,这次我真弄懂了!!!
javascript
brrdg_sefg1 小时前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230941 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_589568101 小时前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安2 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite