享元模式
问题
什么是享元模式?如何区分内部状态和外部状态?前端中有哪些享元模式的实际应用(虚拟列表、对象池、事件委托等)?
答案
享元模式(Flyweight Pattern)是一种结构型设计模式,通过共享已有对象来减少需要创建的对象数量,从而降低内存占用和提高性能。GoF 经典定义为:"运用共享技术有效地支持大量细粒度的对象"。
其核心思想是:当系统中存在大量相似对象时,将对象的**不变部分(内部状态)提取出来共享,而可变部分(外部状态)**由客户端在使用时传入。
核心概念与角色
| 角色 | 说明 |
|---|---|
| Flyweight(享元接口) | 定义享元对象的接口,接收外部状态 |
| ConcreteFlyweight(具体享元) | 存储内部状态,实现享元接口 |
| FlyweightFactory(享元工厂) | 创建和管理享元对象池,确保共享 |
| Client(客户端) | 维护外部状态,通过工厂获取享元对象 |
内部状态 vs 外部状态
这是享元模式最核心的设计决策 —— 正确区分内部状态和外部状态,直接决定了模式能否发挥作用。
| 对比维度 | 内部状态(Intrinsic State) | 外部状态(Extrinsic State) |
|---|---|---|
| 是否共享 | 共享,存储在享元对象内部 | 不共享,由客户端传入 |
| 是否可变 | 不可变,创建后不会改变 | 可变,随环境变化 |
| 存储位置 | 享元对象中 | 客户端或外部数据结构中 |
| 示例 | 棋子颜色、字体样式、图标形状 | 棋子位置、文字内容、坐标 |
问自己两个问题:
- 这个数据在多个对象间是否相同? 相同 -> 内部状态
- 这个数据是否会随着使用场景变化? 变化 -> 外部状态
TypeScript 实现:棋子工厂
以围棋为例:棋盘上可能有数百颗棋子,但颜色只有黑白两种。颜色是内部状态(共享),位置是外部状态(每颗棋子不同)。
// 享元接口
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% 的内存。
通用享元工厂
下面是一个更通用的享元工厂实现,可以用于任何类型的享元对象:
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)是享元模式的变体和延伸,核心思想是预创建一组对象放入池中,使用时借出、用完归还,避免频繁创建和销毁对象的开销。
两者都是复用对象以减少创建开销,但有区别:
- 享元模式:多个客户端同时共享同一个对象(只读共享),通过外部状态区分
- 对象池:对象被独占使用,用完归还后才能被下一个客户端使用
TypeScript 对象池实现
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 节点非常昂贵,可以用对象池复用:
// 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 个),滚动时复用这些节点来展示不同的数据。
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-virtualized 的核心原理就是享元模式:
- 内部状态(共享):DOM 节点结构、样式规则
- 外部状态(可变):每行的数据内容、位置(top/translateY)
相关文档:长列表优化
2. 事件委托
事件委托是享元模式在事件处理中的体现:用一个事件处理器替代 N 个处理器,所有子元素共享同一份事件处理逻辑。
// ❌ 每个按钮单独绑定 -> 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 压力:
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. 字体图标与样式共享
// 字体图标:一套字体文件 → 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 节点池 | 全局配置、日志服务、状态管理 |
| 与客户端关系 | 多个客户端可共享同一享元 | 所有客户端使用同一实例 |
// 单例:全局只有一个 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 不共享
}
}
内存优化效果与适用判断
内存节省计算
// 假设场景:渲染 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%
什么时候值得使用享元模式?
享元模式并非万能,需要满足以下条件才值得使用:
- 系统中有大量相似对象(成百上千个)
- 对象的大部分状态可以外部化(内部状态占比大才有意义)
- 使用享元模式后,外部状态的管理成本不会过高
- 对象创建成本较高(涉及复杂计算、资源加载等)
如果只有几十个对象,或者对象本身很轻量,使用享元模式反而增加了不必要的复杂度。
| 适用场景 | 不适用场景 |
|---|---|
| 大量相似对象(>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 复用原理是什么?和享元模式有什么关系?
答案:
虚拟列表的核心就是享元/对象池思想:
- 有限的 DOM 节点:只创建可视区域内所需的 DOM 节点(如 20-30 个),而不是为每条数据创建节点
- 滚动时复用:滚出可视区域的 DOM 节点被移到底部,填充新的数据
- 享元关系: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;
}
}
}
}
关键优化点:
- 预分配:游戏初始化时一次性创建所有对象
- 标记复用:用
active标志代替创建/销毁 - 零 GC:运行时不产生新对象,避免垃圾回收停顿
Q8: 享元模式在什么情况下不适合使用?
答案:
- 对象数量少(< 100 个):享元工厂本身有管理开销,少量对象直接创建更简单
- 对象状态全部不同:无法提取公共的内部状态进行共享
- 对象很轻量:每个对象只占几十字节,共享节省的内存微乎其微
- 对象状态需要频繁修改:享元对象的内部状态必须不可变,如果需要修改就破坏了共享
- 线程/并发安全难保证:共享对象在多线程环境需要额外同步
- 增加代码复杂度:外部状态的管理、享元工厂的维护增加了系统复杂性
使用享元模式前先做计算:如果节省的内存不到 50%,且对象总数不到几百个,大概率不值得引入享元模式。
Q9: React 中哪些地方体现了享元模式的思想?
答案:
-
合成事件(SyntheticEvent):React 17 之前在 document 上统一代理事件,一个处理器服务所有组件(React 17+ 改为 root 节点)
-
虚拟 DOM 复用:React Diff 算法通过
key尽可能复用已有的 Fiber 节点,而不是销毁重建 -
Context 值共享:同一个 Context 值被所有消费者组件共享,而非每个组件各持一份
-
样式对象提取:
// ❌ 每次渲染创建新的样式对象
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>;
}
- react-window/react-virtualized:虚拟列表通过有限 DOM 节点渲染无限数据
相关文档:内存优化
Q10: 请手写一个支持自动扩容和缩容的对象池
答案:
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,
});