享元模式的核心概念

  1. 享元模式 (Flyweight Pattern):复用已创建的对象,减少内存占用和对象创建开销。它将对象的属性拆分为:
    1. 内部状态:英雄皮肤本身,是固定的、可复用的;
    2. 外部状态:使用皮肤的玩家、使用时间、游戏场次 (每个玩家用的时候都不一样)
    3. 享元池:游戏服务器里存储的皮肤模板 (只存一份,所有玩家共用)
  2. 类图:
  3. 实现代码:
    // ===================== 对应类图中的 HeroSkin 类 =====================
    /**
    * 英雄皮肤类(享元类)
    * 内部状态:skinName(皮肤名称,固定不变,可共享)
    */
    class HeroSkin {
      // 内部状态:皮肤名称(比如 "妲己-女仆咖啡")
      private skinName: string;
    
      // 构造函数:创建皮肤时只传内部状态
      constructor(skinName: string) {
        this.skinName = skinName;
        // 打印提示:只在首次创建时输出,证明复用效果
        console.log(`✅ 游戏服务器创建皮肤模板:${this.skinName}(仅创建1次)`);
      }
    
      // 使用皮肤(接收外部状态:玩家名、游戏时间)
      useSkin(player: string, gameTime: string): void {
        console.log(`
          🎮 游戏对局:
          - 皮肤(内部状态):${this.skinName}
          - 玩家(外部状态):${player}
          - 游戏时间(外部状态):${gameTime}
        `);
      }
    }
    
    // ===================== 对应类图中的 SkinFactory 类 =====================
    /**
    * 皮肤工厂类(管理享元池)
    * 核心作用:相同皮肤只创建一次,后续复用
    */
    class SkinFactory {
      // 享元池:存储已创建的皮肤对象,key=皮肤名称
      private skinPool: Map<string, HeroSkin> = new Map();
    
      // 获取皮肤对象:有则复用,无则创建
      getSkin(skinName: string): HeroSkin {
        // 如果池子里没有这个皮肤,就新建一个并放入池子
        if (!this.skinPool.has(skinName)) {
          this.skinPool.set(skinName, new HeroSkin(skinName));
        }
        // 返回池子里的皮肤对象(复用)
        return this.skinPool.get(skinName)!;
      }
    
      // 查看池子里有多少种皮肤(验证复用效果)
      getSkinCount(): number {
        return this.skinPool.size;
      }
    }
    
    // ===================== 测试代码 =====================
    // 1. 创建皮肤工厂(游戏服务器)
    const skinFactory = new SkinFactory();
    
    // 2. 模拟不同玩家使用相同皮肤(核心:复用皮肤对象)
    // 玩家1:使用妲己-女仆咖啡
    const skin1 = skinFactory.getSkin("妲己-女仆咖啡");
    skin1.useSkin("张三", "2026-03-02 19:00");
    
    // 玩家2:也使用妲己-女仆咖啡(复用已创建的对象)
    const skin2 = skinFactory.getSkin("妲己-女仆咖啡");
    skin2.useSkin("李四", "2026-03-02 19:05");
    
    // 玩家3:使用李白-凤求凰(新建对象)
    const skin3 = skinFactory.getSkin("李白-凤求凰");
    skin3.useSkin("王五", "2026-03-02 19:10");
    
    // 玩家4:也使用李白-凤求凰(复用)
    const skin4 = skinFactory.getSkin("李白-凤求凰");
    skin4.useSkin("赵六", "2026-03-02 19:15");
    
    // 3. 查看池子里的皮肤数量(预期:2种,妲己和李白)
    console.log(`\n📊 服务器中存储的皮肤模板数量:${skinFactory.getSkinCount()} 种`);
    

常用使用场景

虚拟滚动列表

  1. 核心逻辑:虚拟滚动只渲染可视区域的列表项,复用已创建的 DOM / 组件对象 (内部状态:列表项模板 / 结构;外部状态:项数据、位置),避免创建成千上万的 DOM 节点;

  2. 实现代码:

    /**
     * 步骤1:定义列表项享元类(存储可复用的模板/结构)
     * 内部状态:列表项的基础模板(固定不变)
     * 外部状态:项数据、渲染位置
     */
    class ListItemFlyweight {
      // 内部状态:列表项的基础模板(比如li标签的样式/结构)
      private template: string;
    
      constructor(templateType: "default" | "highlight") {
        // 根据类型初始化不同模板(固定结构,可复用)
        this.template = templateType === "highlight" 
          ? `<li class="highlight"></li>` 
          : `<li class="default"></li>`;
        console.log(`✅ 创建列表项模板【${templateType}】(仅创建1次)`);
      }
    
      /**
       * 渲染列表项(结合外部状态)
       * @param data 外部状态:项数据
       * @param position 外部状态:渲染位置(top值)
       */
      render(data: { id: number; text: string }, position: number): void {
        // 模拟DOM渲染:复用模板,仅更新数据和位置
        console.log(`
          📜 渲染列表项:
          - 模板(内部状态):${this.template}
          - 数据(外部状态):id=${data.id},text=${data.text}
          - 位置(外部状态):top=${position}px
        `);
        // 实际开发中这里会操作DOM:更新innerHTML + 设置style.top
      }
    }
    
    /**
     * 步骤2:列表项享元工厂(管理模板复用)
     */
    class ListItemFactory {
      // 享元池:存储不同类型的列表项模板
      private pool: Map<"default" | "highlight", ListItemFlyweight> = new Map();
    
      // 获取模板(复用已有,无则创建)
      getListItem(templateType: "default" | "highlight"): ListItemFlyweight {
        if (!this.pool.has(templateType)) {
          this.pool.set(templateType, new ListItemFlyweight(templateType));
        }
        return this.pool.get(templateType)!;
      }
    
      // 获取池内模板数量(验证复用)
      getPoolSize(): number {
        return this.pool.size;
      }
    }
    
    /**
     * 步骤3:虚拟滚动列表核心逻辑(模拟)
     */
    class VirtualScrollList {
      private factory: ListItemFactory;
      private visibleCount = 10; // 可视区域显示10项
      private itemHeight = 50; // 每项高度50px
    
      constructor() {
        this.factory = new ListItemFactory();
      }
    
      /**
       * 滚动时更新可视区域列表
       * @param scrollTop 滚动距离
       * @param totalData 所有列表数据(模拟10000条)
       */
      update(scrollTop: number, totalData: Array<{ id: number; text: string }>) {
        console.log(`\n🔄 滚动到${scrollTop}px,更新可视区域`);
        // 计算可视区域起始索引
        const startIndex = Math.floor(scrollTop / this.itemHeight);
        // 可视区域数据
        const visibleData = totalData.slice(startIndex, startIndex + this.visibleCount);
    
        // 渲染可视区域项(复用模板)
        visibleData.forEach((item, index) => {
          // 外部状态:渲染位置(top值)
          const position = startIndex * this.itemHeight + index * this.itemHeight;
          // 根据id奇偶性选择模板(模拟不同样式)
          const templateType = item.id % 2 === 0 ? "default" : "highlight";
          // 获取复用的模板对象
          const listItem = this.factory.getListItem(templateType);
          // 渲染(传入外部状态)
          listItem.render(item, position);
        });
      }
    }
    
    // 测试:模拟10000条数据的虚拟滚动
    const totalData = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `列表项${i + 1}` }));
    const virtualList = new VirtualScrollList();
    
    // 模拟滚动到不同位置
    virtualList.update(0, totalData); // 初始位置
    virtualList.update(250, totalData); // 滚动5项
    virtualList.update(1000, totalData); // 滚动20项
    
    // 验证享元池:仅2个模板(default/highlight)
    console.log(`\n📊 享元池模板数量:${virtualList.factory.getPoolSize()} 个`);
    

图表数据点

  1. 核心逻辑:绘制大量数据点 (如散点图、折线图) 时,复用 “数据点样式模板” (内部状态:颜色、大小、形状),仅更新坐标 (外部状态),避免重复创建样式对象;

  2. 实现代码:

    /**
     * 步骤1:数据点享元类(存储可复用的样式模板)
     * 内部状态:数据点样式(颜色、大小、形状)
     * 外部状态:坐标(x/y)、数值
     */
    class DataPointFlyweight {
      // 内部状态:数据点样式(固定可复用)
      private style: { color: string; size: number; shape: "circle" | "square" };
    
      constructor(styleKey: "normal" | "warning" | "success") {
        // 预定义样式模板(内部状态)
        const styleMap = {
          normal: { color: "#666", size: 4, shape: "circle" },
          warning: { color: "#ff9900", size: 6, shape: "circle" },
          success: { color: "#1989fa", size: 5, shape: "square" },
        };
        this.style = styleMap[styleKey];
        console.log(`✅ 创建数据点样式【${styleKey}】(仅创建1次)`);
      }
    
      /**
       * 绘制数据点(结合外部状态)
       * @param coord 外部状态:坐标
       * @param value 外部状态:数值
       */
      draw(coord: { x: number; y: number }, value: number): void {
        console.log(`
          📈 绘制数据点:
          - 样式(内部状态):颜色=${this.style.color},大小=${this.style.size}px,形状=${this.style.shape}
          - 坐标(外部状态):x=${coord.x},y=${coord.y}
          - 数值(外部状态):${value}
        `);
        // 实际Canvas绘制逻辑:ctx.fillStyle = this.style.color; ctx.arc(x,y,size)...
      }
    }
    
    /**
     * 步骤2:数据点享元工厂
     */
    class DataPointFactory {
      private pool: Map<"normal" | "warning" | "success", DataPointFlyweight> = new Map();
    
      getPointStyle(styleKey: "normal" | "warning" | "success"): DataPointFlyweight {
        if (!this.pool.has(styleKey)) {
          this.pool.set(styleKey, new DataPointFlyweight(styleKey));
        }
        return this.pool.get(styleKey)!;
      }
    }
    
    // 测试:绘制1000个数据点(仅3种样式模板)
    const pointFactory = new DataPointFactory();
    // 模拟1000个数据点的坐标和数值
    const points = Array.from({ length: 1000 }, (_, i) => ({
      x: Math.random() * 800,
      y: Math.random() * 600,
      value: Math.random() * 100,
      // 根据数值判断样式类型
      styleKey: i % 3 === 0 ? "warning" : i % 3 === 1 ? "success" : "normal",
    }));
    
    // 绘制所有数据点(复用3种样式模板)
    points.forEach((point) => {
      const flyweight = pointFactory.getPointStyle(point.styleKey);
      flyweight.draw({ x: point.x, y: point.y }, point.value);
    });
    

图标库管理

  1. 核心逻辑:图标库 (如 IconFont、SVG 图标) 复用图标模板 (内部状态:图标名称 / 路径),仅更新大小、颜色、位置 (外部状态),避免重复加载 / 创建图标对象;

  2. 实现代码:

    /**
     * 步骤1:图标享元类(存储可复用的图标模板)
     * 内部状态:图标名称、SVG路径(固定不变)
     * 外部状态:大小、颜色、渲染位置
     */
    class IconFlyweight {
      // 内部状态:图标核心信息(可复用)
      private iconInfo: { name: string; svgPath: string };
    
      constructor(iconName: string) {
        // 模拟SVG路径(实际项目中来自图标库)
        const iconMap = {
          "user": "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z",
          "settings": "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z",
          "search": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"
        };
        this.iconInfo = {
          name: iconName,
          svgPath: iconMap[iconName] || iconMap["user"], // 默认user图标
        };
        console.log(`✅ 加载图标【${iconName}】(仅加载1次)`);
      }
    
      /**
       * 渲染图标(结合外部状态)
       * @param options 外部状态:大小、颜色、位置
       */
      render(options: { size: number; color: string; position: { left: number; top: number } }): void {
        console.log(`
          🖼️ 渲染图标:
          - 图标信息(内部状态):名称=${this.iconInfo.name},SVG路径=${this.iconInfo.svgPath.substring(0, 20)}...
          - 样式(外部状态):大小=${options.size}px,颜色=${options.color}
          - 位置(外部状态):left=${options.position.left}px,top=${options.position.top}px
        `);
        // 实际渲染逻辑:创建SVG元素,设置path、width/height、fill、position等
      }
    }
    
    /**
     * 步骤2:图标工厂(管理图标复用)
     */
    class IconFactory {
      // 享元池:存储已加载的图标
      private iconPool: Map<string, IconFlyweight> = new Map();
    
      /**
      * 获取图标(复用已有,无则加载)
      * @param iconName 图标名称
      */
      getIcon(iconName: string): IconFlyweight {
        if (!this.iconPool.has(iconName)) {
          this.iconPool.set(iconName, new IconFlyweight(iconName));
        }
        return this.iconPool.get(iconName)!;
      }
    
      // 预加载常用图标(项目初始化时)
      preloadIcons(iconNames: string[]): void {
        console.log("\n📦 预加载常用图标:");
        iconNames.forEach(name => this.getIcon(name));
      }
    }
    
    // 测试:图标库使用
    const iconFactory = new IconFactory();
    
    // 1. 预加载常用图标
    iconFactory.preloadIcons(["user", "settings"]);
    
    // 2. 渲染多个相同图标(复用)
    // 渲染用户图标-1
    const userIcon1 = iconFactory.getIcon("user");
    userIcon1.render({ size: 24, color: "#333", position: { left: 10, top: 10 } });
    
    // 渲染用户图标-2(复用,仅改样式/位置)
    const userIcon2 = iconFactory.getIcon("user");
    userIcon2.render({ size: 32, color: "#1989fa", position: { left: 50, top: 50 } });
    
    // 渲染搜索图标(首次加载)
    const searchIcon = iconFactory.getIcon("search");
    searchIcon.render({ size: 20, color: "#666", position: { left: 100, top: 10 } });
    

优缺点

  1. 优点:

    1. ✅ 大大减少内存中的对象数量;
    2. ✅ 提高系统性能,减少 GC 压力;
    3. ✅ 将内部状态外部化,便于管理;
  2. 缺点:

    1. ❌ 需要分离内部和外部状态,增加复杂度;
    2. ❌ 外部状态可能带来运行时开销;
    3. ❌ 线程安全问题 (多线程环境)

适用场景

  1. 大量相似对象:如文档编辑器中的字符;

  2. 对象池化:如数据库连接池、线程池;

  3. 缓存系统:如图片缓存、图标库;

  4. 数据可视化:如图表中的大量数据点;

  5. 游戏开发:如粒子系统、树木渲染;

打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 享元模式的核心概念
  2. 2. 常用使用场景
    1. 2.1. 虚拟滚动列表
    2. 2.2. 图表数据点
    3. 2.3. 图标库管理
  3. 3. 优缺点
  4. 4. 适用场景