享元模式

享元模式(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 🚗🚗🚗

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰8 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy9 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom10 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom10 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试