news 2026/10/3 16:51:14

前端精读:设计模式 - Flyweight 享元模式,用共享化解海量细粒度对象的存储压力

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端精读:设计模式 - Flyweight 享元模式,用共享化解海量细粒度对象的存储压力
  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

享元模式(Flyweight)属于结构型设计模式,核心思想是运用共享技术有效支持大量细粒度对象。本文结合富文本编辑器、网盘秒传、大型多人游戏三个贴近前端与后端实战的场景,完整讲解内部状态与外部状态的分离、FlyweightFactory 的缓存实现、角色划分与适用边界,并给出可直接落地的 TypeScript 代码,帮助你判断"何时该用、何时不该用"享元模式。

先看三个真实场景:什么时候会想到享元

设计模式必须在日常工作里用起来才有价值。原文围绕三个例子展开,它们共同揭示了享元模式的触发条件。

富文本编辑器的字母对象

在英文环境下,富文本编辑器中的文本由大量字母组成。为了便于统一格式化、统一计算(比如统计、排版),我们需要把每个字母都存储为对象,但这样的存储代价非常大。

关键在于:英文字母一共只有 26 个,一篇文档里存在大量重复使用的字母。每个字母除了"位置信息"不同以外,其他信息都是相同且只读的。那么,能不能把文档里成千上万个字母对象的数量降下来?

这正是享元模式的第一个适用信号:对象数量巨大,但可去重后的种类极少。

网盘存储的"秒传"

当我们上传一部几十 GB 的电影时,有时候不到一秒就上传完成了,网盘会提示"已采用极速技术秒传"。为什么不是每次都生效?原因就是:同一部电影可能已经存在于网盘的不同用户、不同文件夹中,服务器只需记录"这部电影已经存在,新用户只是多了一个引用",而无需再存一份完整文件。

和富文本类似,电影文件也是只有"存放位置"不同,其余内容都特别巨大且只读。不同用户看到的是各自文件夹里的电影,但底层共享的是同一份数据。这就是"秒传"背后的原理——共享存储。

大型多人游戏

玩多人游戏时,为了防止外挂,对象创建与计算通常在服务器完成。那如何保证一个玩家拾取物品后,另一个玩家看到的物品会消失?

答案不言而喻:虽然在不同客户端之间,游戏对象看起来是相互独立的,但在同一局游戏中,所有玩家的对象在服务器端是共享的。服务器上同一个武器、同一个怪物对象,被多个客户端共同引用;当一个玩家改变了它的状态(拾取、开火、死亡),其他客户端通过共享引用观察到一致的变化。

意图解释:共享 + 内部状态与外部状态分离

享元模式的意图是:

运用共享技术有效地支持大量细粒度的对象。

"共享"可以理解为缓存:当一个对象创建后,再次访问相同对象时不再创建新的,而只有在访问没有被缓存过的对象时才创建,并立即缓存起来。

这三个例子中的细粒度对象分别是:富文本中的无数字母、网盘中的电影文件、每局游戏中的大量游戏对象。它们都有共同特征:

  • 量特别大,这个很容易理解。
  • 具有大量内部状态,且不随客户端的不同而改变:
    • 富文本的字母,不因为展示到不同语句中而发生变化,变化的只有位置(状态);
    • 电影文件,不因为放在不同用户的文件夹中而对内容产生任何变化,变化的只有归属(属于哪些用户、放在哪些文件夹);
    • 多人游戏中同一把武器对象,不因为有多个人的电脑独立运行而拥有更多弹药,变化的只有"在哪些客户端被访问"。
  • 具有少量外部状态,甚至没有外部状态:字母的位置、电影的位置、游戏对象的客户端归属,都属于外部状态。它们与内部状态相比,体量微乎其微,且方便分离存储。

遇到这种情况,就可以将对象内部状态共享,外部状态独立存储,从而节省大量空间。

原文特别强调了一个容易被忽视的事实:享元模式的价值是全局性的。对网盘公司来说价值巨大(承诺 2TB 空间,用户"下载"了别人分享的 100 部电影,实际可能只增加了 1kb 的存储来记录位置);对整个富文本编辑器来说减少了巨量字母对象;但对于每一个字母对象而言,并没有任何优化。所以享元模式的价值体现在全局,而非单个对象。

结构图:五个角色的职责划分

享元模式由以下角色构成(原文档配图无法在本仓库中展示,此处以文字还原其结构):

  • Flyweight(享元接口):共享接口,通过这个接口可以操作对象的外部状态。
  • ConcreteFlyweight(具体享元):实现 Flyweight 接口的对象,这个对象是可被共享的,内部状态被固化在其中。
  • UnsharedConcreteFlyweight(非共享具体享元):不被共享的对象。享元模式中并不是所有对象都可以被共享,某些对象依然需要独立创建。
  • FlyweightFactory(享元工厂):创建并管理 Flyweight 对象。通过它获取 Flyweight 时,如果已创建,则返回之前创建的那个;没有才创建新的。工厂是享元模式的核心枢纽。
  • Client(客户端):使用 Flyweight 的客户端。

结构图中最关键的一点是:两个不同的 Client 持有的是同一个aConcreteFlyweight引用。这意味着客户端看到的"各自的对象",在内存中实际上是同一个实例——这正是共享带来的空间收益,也是多人游戏"一个玩家拾取后另一个玩家看到消失"的底层原因。

代码例子:从最小实现到完整可运行版本

原文档给出的核心代码用 TypeScript 编写,是享元工厂的最小骨架:

class FlyweightFactory { public getFlyWeight(key) { if (this.flyweight[key]) { return this.flyweight[key] } const flyweight = new Flyweight() this.flyweight[key] = flyweight return flyweight } }

FlyweightFactory提供的getFlyWeight方法,实际上是按照key对flyweight实例进行缓存:相同key下只存储一个flyweight实例。命中缓存直接返回,未命中则创建并立即写入缓存。

为了让这段骨架真正可运行,我们可以补全类型与初始化逻辑,使其具备实际使用价值:

// 享元接口:外部状态通过方法传入,内部状态固化在实例中 interface Flyweight { // 传入外部状态(如字母位置、文件归属),执行共享实例上的操作 render(x: number, y: number): string } // 具体享元:内部状态(如字母字符本身)是只读且共享的 class Character implements Flyweight { constructor(private readonly char: string) {} public render(x: number, y: number): string { // 同一个字符实例被反复复用,位置作为外部状态每次传入 return `<text x="${x}" y="${y}">${this.char}</text>` } } // 享元工厂:以 key 为维度的实例缓存 class FlyweightFactory { private readonly pool: Record<string, Flyweight> = {} public getFlyWeight(key: string): Flyweight { if (this.pool[key]) { return this.pool[key] } const flyweight = new Character(key) this.pool[key] = flyweight return flyweight } public get poolSize(): number { return Object.keys(this.pool).length } } // 使用:渲染 1 万字文章,但内存中只有 26 个字母实例 const factory = new FlyweightFactory() const text = "a quick brown fox jumps over the lazy dog..." const nodes = text.split("").map((char, index) => factory.getFlyWeight(char).render(index * 10, 0) ) console.log(factory.poolSize) // 远小于 text.length,重复字母被共享

可以看到,享元工厂本质是一个"以 key 为索引的对象缓存池",与 设计模式/171.精读《设计模式 - Singleton 单例模式》.md 中"保证一个类仅有一个实例"的单一性不同,享元模式是按 key 维度保证每组 key 只有一个实例,从而在"对象种类有限、使用次数海量"的场景下把实例数从 O(n) 压缩到 O(k)(k 为去重后的种类数)。

前端实战中的享元:对象池与缓存思想

从上面的最小实现可以自然延伸到前端开发中常见的享元应用,这些都属于"共享技术"的具体落地:

  • DOM 虚拟节点 / 组件实例复用:长列表滚动渲染时,只维护窗口内的若干行节点,滚出视口的行被回收并复用给新进入的行——行的"内部状态"(DOM 结构)共享,行号、内容等外部状态单独维护。
  • 资源缓存(图片、字体、图标):同一张图片、同一个 SVG 图标在页面多处使用时,浏览器与框架层面只保留一份资源实例,各处持有引用。
  • 对象池(Object Pool):粒子特效、游戏子弹、动画帧对象等高频创建销毁的对象,使用前从池中取、用完归还,避免频繁 GC。
  • 编译器 / 解析器的 Token 复用:本仓库 编译原理 系列中的手写 SQL 编译器大量使用缓存来避免重复解析计算,其"缓存"思想与享元工厂同源。

需要说明的是,上述应用是享元"共享技术"思想在不同领域的延伸,具体到某一框架是否真正采用享元模式,需要看其源码实现;本文仅从模式思想层面说明其普适性。

弊端:什么时候不要用享元模式

享元模式并非万能,原文明确指出三个不适用场景:

  1. 细粒度对象不多,没必要使用享元模式,引入工厂与缓存只会增加复杂度。
  2. 对象内部状态并不多,主要都是外部状态时,享元模式起不到作用。因为享元模式通过共享对象,只能节省内部状态,而不能节省外部状态——外部状态必须独立存储,共享无法消除它们。
  3. 共享对象数量没有比原始对象少出数量级关系时,意义不大。原文给出了一个非常直观的对比:富文本编辑器对英文文章,1 万字只有 26 个字母,优化比例约 10000:26;而对中文文章,1 万字去重后可能仍有 3000 个汉字,优化比例仅 10000:3000,享元模式的意义就大打折扣。

这三点共同指向一个判断准则:收益 = 共享节约的内部状态体量 × 可去重比例。只有两者都足够大时,享元模式才值得投入。

总结

享元模式的本质就是尽可能共享对象,特别适用于存在大量细粒度对象、而这些对象内部状态特别多、外部状态较少的场景。它的落地方式是 FlyweightFactory 缓存池 + 内部状态共享、外部状态独立传入。

对于云存储来说,享元模式几乎是必须使用的:云存储场景决定了存在大量细粒度文件对象与大量只读文件,非常适合共享一个对象,每个用户存储的只是引用。同理,富文本编辑器、大型多人游戏、长列表渲染,都是享元模式发挥价值的典型领域。

判断是否使用享元模式,请记住原文给出的三个反问:对象量够大吗?内部状态占比够高吗?共享后数量能降一个数量级吗?三个答案都是"是",才值得引入。

本文基于 设计模式/177.精读《设计模式 - Flyweight 享元模式》.md 展开,属于 readme.md 所收录"前端精读"周刊的设计模式系列之一。同系列还可延伸阅读单例模式(关注唯一性)、代理模式(关注访问控制)等结构型与创建型模式,组合理解更能把握模式之间的取舍。

  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 16:48:33

【大数据毕设推荐】K-Means与FP-Growth算法实战:电动汽车品牌与车型流行趋势数据分析系统源码解析 毕业设计 选题推荐 毕设选题 数据分析 机器学习

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡如果你遇到具体的…

作者头像 李华
网站建设 2026/10/3 16:46:18

2026年AI动漫短剧制作完整教程:从剧本到成片的8个环节与验收清单

本文回答&#xff1a;AI 动漫短剧从剧本到成片要过哪 8 个环节&#xff0c;每个环节产出什么、按什么验收&#xff0c;Seedance 2.0 与 2.5 两版提示词规范怎么分开写&#xff0c;以及自己用工具做、调接口、用 AI 视频产线、外包几种做法怎么选。 ai动漫短剧教程的核心是一条从…

作者头像 李华
网站建设 2026/10/3 16:42:58

步进电机控制方案:DRV8818PWPR与PIC24FJ128GA310的协同设计

做步进电机控制的这些年&#xff0c;我越来越觉得“芯片选型”这件事比写代码更重要。DRV8818PWPR这颗驱动芯片搭配PIC24FJ128GA310这颗 16 位单片机&#xff0c;是我在工业机器人和自动化设备项目里反复验证过的一套组合&#xff1a;一个负责把电流变成力矩&#xff0c;一个负…

作者头像 李华