跳到主要内容

享元模式

问题

什么是享元模式?如何区分内部状态和外部状态?前端中有哪些享元模式的实际应用(虚拟列表、对象池、事件委托等)?

答案

享元模式(Flyweight Pattern)是一种结构型设计模式,通过共享已有对象来减少需要创建的对象数量,从而降低内存占用和提高性能。GoF 经典定义为:"运用共享技术有效地支持大量细粒度的对象"。

其核心思想是:当系统中存在大量相似对象时,将对象的**不变部分(内部状态)提取出来共享,而可变部分(外部状态)**由客户端在使用时传入。


核心概念与角色

角色说明
Flyweight(享元接口)定义享元对象的接口,接收外部状态
ConcreteFlyweight(具体享元)存储内部状态,实现享元接口
FlyweightFactory(享元工厂)创建和管理享元对象池,确保共享
Client(客户端)维护外部状态,通过工厂获取享元对象

内部状态 vs 外部状态

这是享元模式最核心的设计决策 —— 正确区分内部状态和外部状态,直接决定了模式能否发挥作用。

对比维度内部状态(Intrinsic State)外部状态(Extrinsic State)
是否共享共享,存储在享元对象内部不共享,由客户端传入
是否可变不可变,创建后不会改变可变,随环境变化
存储位置享元对象中客户端或外部数据结构中
示例棋子颜色、字体样式、图标形状棋子位置、文字内容、坐标
判断技巧

问自己两个问题:

  1. 这个数据在多个对象间是否相同? 相同 -> 内部状态
  2. 这个数据是否会随着使用场景变化? 变化 -> 外部状态

TypeScript 实现:棋子工厂

以围棋为例:棋盘上可能有数百颗棋子,但颜色只有黑白两种。颜色是内部状态(共享),位置是外部状态(每颗棋子不同)。

flyweight/chess.ts
// 享元接口
interface ChessPiece {
color: string;
render(x: number, y: number): void;
}

// 具体享元:只存储内部状态(颜色)
class ConcreteChessPiece implements ChessPiece {
readonly color: string; // 内部状态:不可变,可共享

constructor(color: string) {
this.color = color;
// 模拟复杂初始化(加载纹理、计算渲染参数等)
console.log(`创建${color}棋子(耗时操作)`);
}

render(x: number, y: number): void { // 外部状态通过参数传入
console.log(`在 (${x}, ${y}) 渲染${this.color}棋子`);
}
}

// 享元工厂:管理共享对象
class ChessPieceFactory {
private static pieces: Map<string, ChessPiece> = new Map();

static getPiece(color: string): ChessPiece {
if (!this.pieces.has(color)) {
this.pieces.set(color, new ConcreteChessPiece(color));
}
return this.pieces.get(color)!;
}

static getCount(): number {
return this.pieces.size;
}
}

// 客户端:维护外部状态
interface ChessPosition {
piece: ChessPiece;
x: number;
y: number;
}

const board: ChessPosition[] = [];

// 放置 100 颗黑棋和 100 颗白棋
for (let i = 0; i < 100; i++) {
board.push({
piece: ChessPieceFactory.getPiece('黑'), // 共享同一个黑棋对象
x: Math.floor(i / 19),
y: i % 19,
});
}
for (let i = 0; i < 100; i++) {
board.push({
piece: ChessPieceFactory.getPiece('白'), // 共享同一个白棋对象
x: Math.floor(i / 19),
y: i % 19,
});
}

console.log(`棋盘上有 ${board.length} 颗棋子`); // 200
console.log(`实际创建了 ${ChessPieceFactory.getCount()} 个享元对象`); // 2
内存节省

200 颗棋子只创建了 2 个享元对象。如果每个棋子对象占用 1KB(含纹理数据),传统方式需要 200KB,享元模式只需 2KB + 200 个位置引用(约 3.2KB),节省约 97% 的内存


通用享元工厂

下面是一个更通用的享元工厂实现,可以用于任何类型的享元对象:

flyweight/generic-factory.ts
class FlyweightFactory<T> {
private flyweights: Map<string, T> = new Map();
private factory: (key: string) => T;

constructor(factory: (key: string) => T) {
this.factory = factory;
}

get(key: string): T {
if (!this.flyweights.has(key)) {
this.flyweights.set(key, this.factory(key));
}
return this.flyweights.get(key)!;
}

get size(): number {
return this.flyweights.size;
}

has(key: string): boolean {
return this.flyweights.has(key);
}

clear(): void {
this.flyweights.clear();
}
}

// 使用示例:字体样式享元
interface FontStyle {
fontFamily: string;
fontSize: number;
fontWeight: string;
}

const fontFactory = new FlyweightFactory<FontStyle>((key: string) => {
const [family, size, weight] = key.split('_');
return { fontFamily: family, fontSize: Number(size), fontWeight: weight };
});

// 大量文本节点可以共享相同的字体样式对象
const style1 = fontFactory.get('Arial_14_bold');
const style2 = fontFactory.get('Arial_14_bold');
console.log(style1 === style2); // true,同一个引用

对象池模式

对象池(Object Pool)是享元模式的变体和延伸,核心思想是预创建一组对象放入池中,使用时借出、用完归还,避免频繁创建和销毁对象的开销。

享元模式 vs 对象池

两者都是复用对象以减少创建开销,但有区别:

  • 享元模式:多个客户端同时共享同一个对象(只读共享),通过外部状态区分
  • 对象池:对象被独占使用,用完归还后才能被下一个客户端使用

TypeScript 对象池实现

pool/object-pool.ts
class ObjectPool<T> {
private available: T[] = []; // 空闲对象
private inUse: Set<T> = new Set(); // 使用中的对象
private factory: () => T; // 工厂函数
private reset: (obj: T) => void; // 重置函数
private maxSize: number;

constructor(options: {
factory: () => T;
reset?: (obj: T) => void;
initialSize?: number;
maxSize?: number;
}) {
this.factory = options.factory;
this.reset = options.reset ?? (() => {});
this.maxSize = options.maxSize ?? Infinity;

// 预创建对象
const initialSize = options.initialSize ?? 0;
for (let i = 0; i < initialSize; i++) {
this.available.push(this.factory());
}
}

acquire(): T | null {
let obj: T;

if (this.available.length > 0) {
obj = this.available.pop()!;
} else if (this.inUse.size < this.maxSize) {
obj = this.factory();
} else {
return null; // 池已满
}

this.inUse.add(obj);
return obj;
}

release(obj: T): void {
if (!this.inUse.has(obj)) return;

this.inUse.delete(obj);
this.reset(obj); // 重置对象状态
this.available.push(obj); // 归还到池中
}

get stats() {
return {
available: this.available.length,
inUse: this.inUse.size,
total: this.available.length + this.inUse.size,
};
}
}

实战:DOM 元素池

在长列表或游戏场景中,频繁创建和销毁 DOM 节点非常昂贵,可以用对象池复用:

pool/dom-pool.ts
// DOM 元素对象池
const divPool = new ObjectPool<HTMLDivElement>({
factory: () => document.createElement('div'),
reset: (div) => {
div.className = '';
div.innerHTML = '';
div.removeAttribute('style');
// 从 DOM 中移除(如果挂载了的话)
div.parentNode?.removeChild(div);
},
initialSize: 20,
maxSize: 100,
});

// 使用
function showNotification(message: string): void {
const div = divPool.acquire();
if (!div) return;

div.className = 'notification';
div.textContent = message;
document.body.appendChild(div);

setTimeout(() => {
divPool.release(div); // 3 秒后归还到池中
}, 3000);
}

前端实际应用

1. 虚拟列表 DOM 复用

虚拟列表是享元/对象池思想在前端最典型的应用。面对 10 万条数据,只渲染可视区域内的少量 DOM 节点(通常 20-30 个),滚动时复用这些节点来展示不同的数据。

virtual-list/simple-virtual-list.ts
interface VirtualListOptions {
container: HTMLElement;
itemHeight: number;
totalItems: number;
renderItem: (index: number, el: HTMLElement) => void;
}

class SimpleVirtualList {
private container: HTMLElement;
private itemHeight: number;
private totalItems: number;
private renderItem: VirtualListOptions['renderItem'];
private pool: HTMLElement[] = []; // DOM 节点池
private visibleCount: number;

constructor(options: VirtualListOptions) {
this.container = options.container;
this.itemHeight = options.itemHeight;
this.totalItems = options.totalItems;
this.renderItem = options.renderItem;

const containerHeight = this.container.clientHeight;
this.visibleCount = Math.ceil(containerHeight / this.itemHeight) + 2; // 缓冲

this.init();
}

private init(): void {
// 设置滚动容器
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';

// 撑开总高度
const spacer = document.createElement('div');
spacer.style.height = `${this.totalItems * this.itemHeight}px`;
this.container.appendChild(spacer);

// 预创建有限的 DOM 节点(享元思想)
for (let i = 0; i < this.visibleCount; i++) {
const item = document.createElement('div');
item.style.position = 'absolute';
item.style.height = `${this.itemHeight}px`;
item.style.width = '100%';
this.pool.push(item);
this.container.appendChild(item);
}

this.container.addEventListener('scroll', () => this.onScroll());
this.onScroll(); // 初始渲染
}

private onScroll(): void {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);

// 复用池中的 DOM 节点展示不同数据
this.pool.forEach((el, i) => {
const dataIndex = startIndex + i;
if (dataIndex < this.totalItems) {
el.style.top = `${dataIndex * this.itemHeight}px`;
el.style.display = 'block';
this.renderItem(dataIndex, el); // 用外部状态更新节点
} else {
el.style.display = 'none';
}
});
}
}

// 使用:10 万条数据,只创建约 22 个 DOM 节点
const list = new SimpleVirtualList({
container: document.getElementById('list')!,
itemHeight: 50,
totalItems: 100000,
renderItem: (index, el) => {
el.textContent = `Item #${index}`;
},
});
react-window 的本质

react-window 和 react-virtualized 的核心原理就是享元模式:

  • 内部状态(共享):DOM 节点结构、样式规则
  • 外部状态(可变):每行的数据内容、位置(top/translateY)

相关文档:长列表优化

2. 事件委托

事件委托是享元模式在事件处理中的体现:用一个事件处理器替代 N 个处理器,所有子元素共享同一份事件处理逻辑。

flyweight/event-delegation.ts
// ❌ 每个按钮单独绑定 -> 1000 个处理器
document.querySelectorAll('.btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
handleClick(target.dataset.action!);
});
});

// ✅ 事件委托 -> 1 个处理器(享元思想)
document.getElementById('toolbar')!.addEventListener('click', (e) => {
const target = (e.target as HTMLElement).closest('[data-action]');
if (target) {
const action = (target as HTMLElement).dataset.action!;
handleClick(action); // 共享同一份处理逻辑,action 是外部状态
}
});

function handleClick(action: string): void {
console.log(`执行操作: ${action}`);
}

3. Canvas 游戏中的粒子/子弹对象池

在游戏开发中,子弹、粒子等对象会频繁创建和销毁,使用对象池可以显著减少 GC 压力:

pool/particle-pool.ts
interface Particle {
x: number;
y: number;
vx: number;
vy: number;
life: number;
active: boolean;
}

class ParticleSystem {
private pool: Particle[] = [];
private maxParticles: number;

constructor(maxParticles: number = 1000) {
this.maxParticles = maxParticles;
// 预创建所有粒子
for (let i = 0; i < maxParticles; i++) {
this.pool.push({
x: 0, y: 0, vx: 0, vy: 0,
life: 0, active: false,
});
}
}

emit(x: number, y: number): Particle | null {
// 从池中找到不活跃的粒子进行复用
const particle = this.pool.find((p) => !p.active);
if (!particle) return null;

particle.x = x;
particle.y = y;
particle.vx = (Math.random() - 0.5) * 4;
particle.vy = (Math.random() - 0.5) * 4;
particle.life = 60;
particle.active = true;
return particle;
}

update(): void {
for (const p of this.pool) {
if (!p.active) continue;
p.x += p.vx;
p.y += p.vy;
p.life--;
if (p.life <= 0) {
p.active = false; // 归还到池中,不销毁
}
}
}

render(ctx: CanvasRenderingContext2D): void {
for (const p of this.pool) {
if (!p.active) continue;
ctx.fillStyle = `rgba(255, 100, 50, ${p.life / 60})`;
ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
}
}
}

4. 字体图标与样式共享

flyweight/icon-font.ts
// 字体图标:一套字体文件 → N 个图标(共享字体数据)
// 内部状态:字体文件(@font-face 中加载一次)
// 外部状态:每个图标的 Unicode 码点、大小、颜色

// CSS 类复用 vs 内联样式也是享元思想
// ❌ 内联样式:每个元素各自持有样式对象
// elements.forEach(el => {
// el.style.color = 'red';
// el.style.fontSize = '14px';
// });

// ✅ CSS 类:所有元素共享一条样式规则
// .highlight { color: red; font-size: 14px; }
// 内部状态:样式规则
// 外部状态:哪些元素使用这个类名

// 图片懒加载中的占位图复用
class PlaceholderManager {
private static placeholders: Map<string, HTMLImageElement> = new Map();

static getPlaceholder(size: string): HTMLImageElement {
if (!this.placeholders.has(size)) {
const img = new Image();
img.src = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"><rect fill="%23eee" width="100%" height="100%"/></svg>`;
this.placeholders.set(size, img);
}
// 返回同一个占位图引用,多个位置共享
return this.placeholders.get(size)!;
}
}

享元模式 vs 单例模式

享元模式和单例模式都涉及对象复用,但设计意图完全不同:

对比维度享元模式单例模式
实例数量多个共享实例(按内部状态分类)唯一实例
目的减少相似对象数量,节省内存确保全局只有一个实例
状态区分内部/外部状态维护自身完整状态
创建方式由工厂根据 key 创建不同实例由类自身控制唯一性
典型场景棋子、粒子、DOM 节点池全局配置、日志服务、状态管理
与客户端关系多个客户端可共享同一享元所有客户端使用同一实例
compare/singleton-vs-flyweight.ts
// 单例:全局只有一个 Store
class Store {
private static instance: Store;
static getInstance(): Store {
if (!Store.instance) Store.instance = new Store();
return Store.instance;
}
}

// 享元:可能有多个共享实例(按颜色分类)
class ChessFactory {
private static pieces = new Map<string, ChessPiece>();
static get(color: string): ChessPiece {
if (!this.pieces.has(color)) {
this.pieces.set(color, new ConcreteChessPiece(color));
}
return this.pieces.get(color)!; // 相同 color 共享,不同 color 不共享
}
}

内存优化效果与适用判断

内存节省计算

flyweight/memory-calculation.ts
// 假设场景:渲染 10,000 个图标
const ICON_COUNT = 10000;
const ICON_TYPES = 50; // 50 种图标类型

// 每个图标对象的内存占用
const SHARED_DATA_SIZE = 2048; // 内部状态:SVG 路径、渲染数据等(2KB)
const UNIQUE_DATA_SIZE = 32; // 外部状态:位置、大小、颜色引用(32B)
const REF_SIZE = 8; // 对象引用大小(8B)

// ❌ 不使用享元模式
const withoutFlyweight = ICON_COUNT * (SHARED_DATA_SIZE + UNIQUE_DATA_SIZE);
// = 10,000 * 2,080 = 20,800,000 B ≈ 19.8 MB

// ✅ 使用享元模式
const withFlyweight =
ICON_TYPES * SHARED_DATA_SIZE + // 50 个享元对象
ICON_COUNT * (UNIQUE_DATA_SIZE + REF_SIZE); // 10,000 个外部状态 + 引用
// = 50 * 2,048 + 10,000 * 40 = 102,400 + 400,000 = 502,400 B ≈ 490 KB

const savings = ((1 - withFlyweight / withoutFlyweight) * 100).toFixed(1);
console.log(`节省内存: ${savings}%`); // 节省内存: 97.6%

什么时候值得使用享元模式?

使用条件

享元模式并非万能,需要满足以下条件才值得使用:

  1. 系统中有大量相似对象(成百上千个)
  2. 对象的大部分状态可以外部化(内部状态占比大才有意义)
  3. 使用享元模式后,外部状态的管理成本不会过高
  4. 对象创建成本较高(涉及复杂计算、资源加载等)

如果只有几十个对象,或者对象本身很轻量,使用享元模式反而增加了不必要的复杂度。

适用场景不适用场景
大量相似对象(>100)对象数量少
内部状态占比大每个对象状态都不同
创建/销毁开销大对象轻量
内存是瓶颈CPU 是瓶颈

常见面试问题

Q1: 什么是享元模式?它解决什么问题?

答案

享元模式是一种结构型设计模式,通过共享对象来减少内存中的对象数量。它将对象的状态分为内部状态(不变、可共享)和外部状态(可变、不共享),把内部状态相同的对象合并为一个共享实例,外部状态由客户端在使用时传入。

核心问题:当系统中存在大量细粒度对象时,如果每个对象都独立创建,会消耗大量内存。享元模式通过共享减少对象数量,从而降低内存占用。

// 没有享元:10000 个棋子 = 10000 个对象
// 有享元:10000 个棋子 = 2 个享元对象(黑/白)+ 10000 个位置数据

Q2: 如何区分内部状态和外部状态?

答案

判断标准内部状态外部状态
多个对象间是否相同相同不同
是否随使用场景变化不变变化
能否被多对象共享不能

常见的内部/外部状态划分:

// 文本编辑器中的字符
// 内部状态:字体、大小、样式(可能上千个字符用同一套)
// 外部状态:字符内容、位置(每个字符都不同)

interface CharFlyweight {
font: string; // 内部
size: number; // 内部
bold: boolean; // 内部
}

interface CharContext {
char: string; // 外部
row: number; // 外部
col: number; // 外部
flyweight: CharFlyweight; // 引用共享的享元
}

Q3: 享元模式和单例模式有什么区别?

答案

维度享元模式单例模式
实例数量多个(按类别)唯一
控制维度按内部状态的组合分类全局唯一性
设计目的减少对象数量、节省内存保证全局一个实例
状态处理内部/外部分离管理自身完整状态

简单理解:单例是"全局只有一个",享元是"相同类型共享一个"。享元工厂管理的是一组享元对象(可以有多个不同的),单例类只有一个实例。

Q4: 虚拟列表的 DOM 复用原理是什么?和享元模式有什么关系?

答案

虚拟列表的核心就是享元/对象池思想:

  1. 有限的 DOM 节点:只创建可视区域内所需的 DOM 节点(如 20-30 个),而不是为每条数据创建节点
  2. 滚动时复用:滚出可视区域的 DOM 节点被移到底部,填充新的数据
  3. 享元关系:DOM 结构是内部状态(共享),数据内容和位置是外部状态
// react-window 的简化原理
function VirtualList({ items, height, itemHeight }: Props) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const visibleCount = Math.ceil(height / itemHeight) + 1;

// 只渲染 visibleCount 个节点,而非 items.length 个
return (
<div style={{ height, overflow: 'auto' }} onScroll={handleScroll}>
<div style={{ height: items.length * itemHeight }}>
{Array.from({ length: visibleCount }, (_, i) => {
const index = startIndex + i;
return index < items.length ? (
<div key={i} style={{ position: 'absolute', top: index * itemHeight }}>
{items[index].content}
</div>
) : null;
})}
</div>
</div>
);
}

相关文档:长列表优化

Q5: 对象池模式和享元模式有什么区别和联系?

答案

维度享元模式对象池模式
使用方式多个客户端同时共享同一对象对象被独占使用,用完归还
并发性支持并发访问(对象只读)同一对象同一时间只有一个使用者
对象状态内部状态不可变对象状态在使用间会被重置
典型场景棋子颜色、字体样式连接池、线程池、DOM 元素池
关系强调共享强调复用

两者都是减少对象创建,但享元是"共享"(多人同时用一个),对象池是"轮用"(一人用完换下一人)。

Q6: 事件委托为什么是享元模式的体现?

答案

事件委托将 N 个子元素的事件处理器合并为父元素上的 1 个处理器:

  • 内部状态(共享):事件处理逻辑
  • 外部状态(可变):具体触发事件的目标元素(event.target
// 1000 个列表项只需 1 个处理器
const list = document.getElementById('list')!;

list.addEventListener('click', (e: MouseEvent) => {
const target = (e.target as HTMLElement).closest('li');
if (!target) return;

// 共享同一份处理逻辑
const id = target.dataset.id; // 外部状态:目标元素
const action = target.dataset.action; // 外部状态:操作类型
handleItemAction(id!, action!);
});

这也是 React 合成事件系统的设计原理之一:React 在根节点统一注册事件,而不是在每个组件上绑定。

Q7: 在 Canvas 游戏开发中,如何用对象池优化粒子效果?

答案

粒子效果的特点是高频创建和销毁(爆炸、拖尾等),直接 new/delete 会导致频繁的 GC(垃圾回收),引发卡顿。

对象池的做法:

class BulletPool {
private bullets: Array<{
x: number; y: number;
dx: number; dy: number;
active: boolean;
}>;

constructor(size: number) {
// 预分配所有子弹对象
this.bullets = Array.from({ length: size }, () => ({
x: 0, y: 0, dx: 0, dy: 0, active: false,
}));
}

fire(x: number, y: number, dx: number, dy: number): boolean {
const bullet = this.bullets.find((b) => !b.active);
if (!bullet) return false;

// 重置状态而非创建新对象
bullet.x = x;
bullet.y = y;
bullet.dx = dx;
bullet.dy = dy;
bullet.active = true;
return true;
}

update(): void {
for (const b of this.bullets) {
if (!b.active) continue;
b.x += b.dx;
b.y += b.dy;
// 越界则"回收"
if (b.x < 0 || b.x > 800 || b.y < 0 || b.y > 600) {
b.active = false;
}
}
}
}

关键优化点:

  1. 预分配:游戏初始化时一次性创建所有对象
  2. 标记复用:用 active 标志代替创建/销毁
  3. 零 GC:运行时不产生新对象,避免垃圾回收停顿

Q8: 享元模式在什么情况下不适合使用?

答案

  1. 对象数量少(< 100 个):享元工厂本身有管理开销,少量对象直接创建更简单
  2. 对象状态全部不同:无法提取公共的内部状态进行共享
  3. 对象很轻量:每个对象只占几十字节,共享节省的内存微乎其微
  4. 对象状态需要频繁修改:享元对象的内部状态必须不可变,如果需要修改就破坏了共享
  5. 线程/并发安全难保证:共享对象在多线程环境需要额外同步
  6. 增加代码复杂度:外部状态的管理、享元工厂的维护增加了系统复杂性
权衡原则

使用享元模式前先做计算:如果节省的内存不到 50%,且对象总数不到几百个,大概率不值得引入享元模式。

Q9: React 中哪些地方体现了享元模式的思想?

答案

  1. 合成事件(SyntheticEvent):React 17 之前在 document 上统一代理事件,一个处理器服务所有组件(React 17+ 改为 root 节点)

  2. 虚拟 DOM 复用:React Diff 算法通过 key 尽可能复用已有的 Fiber 节点,而不是销毁重建

  3. Context 值共享:同一个 Context 值被所有消费者组件共享,而非每个组件各持一份

  4. 样式对象提取

// ❌ 每次渲染创建新的样式对象
function Bad() {
return <div style={{ color: 'red', fontSize: 14 }}>text</div>;
}

// ✅ 共享样式对象(享元思想)
const styles = { color: 'red', fontSize: 14 } as const;
function Good() {
return <div style={styles}>text</div>;
}
  1. react-window/react-virtualized:虚拟列表通过有限 DOM 节点渲染无限数据

相关文档:内存优化

Q10: 请手写一个支持自动扩容和缩容的对象池

答案

pool/auto-scaling-pool.ts
class AutoScalingPool<T> {
private available: T[] = [];
private inUse: Set<T> = new Set();
private factory: () => T;
private destroy: (obj: T) => void;
private minSize: number;
private maxSize: number;
private shrinkTimer: ReturnType<typeof setInterval> | null = null;

constructor(options: {
factory: () => T;
destroy?: (obj: T) => void;
minSize?: number;
maxSize?: number;
shrinkInterval?: number; // 缩容检查间隔(ms)
}) {
this.factory = options.factory;
this.destroy = options.destroy ?? (() => {});
this.minSize = options.minSize ?? 5;
this.maxSize = options.maxSize ?? 50;

// 预创建最小数量
for (let i = 0; i < this.minSize; i++) {
this.available.push(this.factory());
}

// 定期缩容
const interval = options.shrinkInterval ?? 30000;
this.shrinkTimer = setInterval(() => this.shrink(), interval);
}

acquire(): T | null {
if (this.available.length > 0) {
const obj = this.available.pop()!;
this.inUse.add(obj);
return obj;
}

// 自动扩容
if (this.totalSize < this.maxSize) {
const obj = this.factory();
this.inUse.add(obj);
return obj;
}

return null; // 已达上限
}

release(obj: T): void {
if (!this.inUse.delete(obj)) return;
this.available.push(obj);
}

private shrink(): void {
// 保持空闲对象不超过 minSize
while (this.available.length > this.minSize) {
const obj = this.available.pop()!;
this.destroy(obj); // 真正释放资源
}
}

private get totalSize(): number {
return this.available.length + this.inUse.size;
}

dispose(): void {
if (this.shrinkTimer) clearInterval(this.shrinkTimer);
this.available.forEach((obj) => this.destroy(obj));
this.inUse.forEach((obj) => this.destroy(obj));
this.available = [];
this.inUse.clear();
}
}

// 使用示例:WebSocket 连接池
const wsPool = new AutoScalingPool<WebSocket>({
factory: () => new WebSocket('wss://example.com'),
destroy: (ws) => ws.close(),
minSize: 2,
maxSize: 10,
shrinkInterval: 60000,
});

相关链接