ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

前端精读:设计模式 - 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: Recordstring, 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 万字只有 26 个字母优化比例约 10000:26而对中文文章1 万字去重后可能仍有 3000 个汉字优化比例仅 10000:3000享元模式的意义就大打折扣。这三点共同指向一个判断准则收益 共享节约的内部状态体量 × 可去重比例。只有两者都足够大时享元模式才值得投入。总结享元模式的本质就是尽可能共享对象特别适用于存在大量细粒度对象、而这些对象内部状态特别多、外部状态较少的场景。它的落地方式是 FlyweightFactory 缓存池 内部状态共享、外部状态独立传入。对于云存储来说享元模式几乎是必须使用的云存储场景决定了存在大量细粒度文件对象与大量只读文件非常适合共享一个对象每个用户存储的只是引用。同理富文本编辑器、大型多人游戏、长列表渲染都是享元模式发挥价值的典型领域。判断是否使用享元模式请记住原文给出的三个反问对象量够大吗内部状态占比够高吗共享后数量能降一个数量级吗三个答案都是是才值得引入。本文基于 设计模式/177.精读《设计模式 - Flyweight 享元模式》.md 展开属于 readme.md 所收录前端精读周刊的设计模式系列之一。同系列还可延伸阅读单例模式关注唯一性、代理模式关注访问控制等结构型与创建型模式组合理解更能把握模式之间的取舍。赞分享文档技术博客教程【免费下载链接】weekly前端精读周刊。帮你理解最前沿、实用的技术。项目地址https://gitcode.com/GitHub_Trending/we/weekly点击查看免费下载相关推荐CS-Notes 设计模式指南享元模式Flyweight如何用共享内部状态支撑海量细粒度对象CS Notes 设计模式指南享元模式Flyweight如何用共享内部状态支撑海量细粒度对象 享元模式Flyweight是一种用于减少内存开销的结知识库文档教程Unity3DTraining 设计模式实战享元模式Flyweight源码级解析——用共享技术支持大量细粒度对象Unity3DTraining 设计模式实战享元模式Flyweight源码级解析——用共享技术支持大量细粒度对象 在游戏与图形密集型应用中对象太多导致示例工程Composio享元模式共享细粒度对象的技术Composio享元模式共享细粒度对象的技术 什么是享元模式Flyweight Pattern 享元模式是一种结构型设计模式它通过共享细粒度对象来有效支人工智能AI Agent工具调用MCP 服务MCP Clients创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表