享元模式的核心概念
- 享元模式 (Flyweight Pattern):复用已创建的对象,减少内存占用和对象创建开销。它将对象的属性拆分为:
- 内部状态:英雄皮肤本身,是固定的、可复用的;
- 外部状态:使用皮肤的玩家、使用时间、游戏场次 (每个玩家用的时候都不一样);
- 享元池:游戏服务器里存储的皮肤模板 (只存一份,所有玩家共用);
- 类图:
- 实现代码:
// ===================== 对应类图中的 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()} 种`);
常用使用场景
虚拟滚动列表
-
核心逻辑:虚拟滚动只渲染可视区域的列表项,复用已创建的 DOM / 组件对象 (内部状态:列表项模板 / 结构;外部状态:项数据、位置),避免创建成千上万的 DOM 节点;
-
实现代码:
/** * 步骤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:数据点享元类(存储可复用的样式模板) * 内部状态:数据点样式(颜色、大小、形状) * 外部状态:坐标(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); });
图标库管理
-
核心逻辑:图标库 (如 IconFont、SVG 图标) 复用图标模板 (内部状态:图标名称 / 路径),仅更新大小、颜色、位置 (外部状态),避免重复加载 / 创建图标对象;
-
实现代码:
/** * 步骤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 } });
优缺点
-
优点:
- ✅ 大大减少内存中的对象数量;
- ✅ 提高系统性能,减少 GC 压力;
- ✅ 将内部状态外部化,便于管理;
-
缺点:
- ❌ 需要分离内部和外部状态,增加复杂度;
- ❌ 外部状态可能带来运行时开销;
- ❌ 线程安全问题 (多线程环境);
适用场景
-
大量相似对象:如文档编辑器中的字符;
-
对象池化:如数据库连接池、线程池;
-
缓存系统:如图片缓存、图标库;
-
数据可视化:如图表中的大量数据点;
-
游戏开发:如粒子系统、树木渲染;
命令模式(行为型模式)
上一篇