设计模式
What — 是什么
设计模式是解决软件设计中常见问题的可复用方案。前端最常用的模式包括观察者、策略、单例、代理、发布订阅等。
核心模式:
- 观察者模式:对象间一对多依赖,状态变化自动通知
- 发布订阅模式:通过事件中心解耦发布者和订阅者
- 策略模式:定义一系列算法,封装后可互换
- 单例模式:确保一个类只有一个实例
- 代理模式:控制对对象的访问,添加额外逻辑
- 装饰器模式:动态给对象添加职责,不改变原对象
- 工厂模式:创建对象不暴露创建逻辑
- 适配器模式:转换接口使其兼容
关键特性:
- 前端场景与后端不同,侧重点在于 UI 交互、异步流、模块通信
- 框架内部大量使用设计模式(Vue 响应式 = 观察者,React HOC = 装饰器,Proxy = 代理)
- 不要为了模式而模式,先有问题再选模式
Why — 为什么
适用场景:
- 复杂交互逻辑的组织
- 模块间解耦
- 代码可扩展性和可维护性
模式对比:
| 模式 | 核心思想 | 典型前端场景 |
|---|---|---|
| 观察者 | 状态变化自动通知 | Vue 响应式系统、EventTarget |
| 发布订阅 | 事件中心解耦 | EventEmitter、Vuex/Pinia |
| 策略 | 算法可互换 | 表单验证、折扣计算 |
| 单例 | 全局唯一实例 | 全局状态、缓存、WebSocket 连接 |
| 代理 | 控制访问 | Vue3 Proxy、图片懒加载 |
| 装饰器 | 动态增强 | React HOC、AOP 日志 |
| 工厂 | 隐藏创建细节 | 组件动态创建、配置化渲染 |
| 适配器 | 接口转换 | 旧 API 兼容、第三方 SDK 封装 |
How — 怎么用
观察者模式
// 被观察者:维护观察者列表,状态变化时通知
class Observable<T> {
private observers: Set<(value: T) => void> = new Set();
private _value: T;
constructor(initialValue: T) {
this._value = initialValue;
}
get value() { return this._value; }
set value(newValue: T) {
this._value = newValue;
this.observers.forEach(fn => fn(newValue));
}
subscribe(fn: (value: T) => void) {
this.observers.add(fn);
fn(this._value); // 立即执行一次(类似 Vue watch immediate)
return () => this.observers.delete(fn); // 返回取消函数
}
}
// 使用
const searchQuery = new Observable('');
const unsubscribe = searchQuery.subscribe(query => {
fetchResults(query);
});
searchQuery.value = 'hello'; // 自动触发 fetchResults
unsubscribe(); // 取消订阅
发布订阅模式
class EventBus {
private events: Map<string, Set<Function>> = new Map();
on(event: string, handler: Function) {
if (!this.events.has(event)) this.events.set(event, new Set());
this.events.get(event)!.add(handler);
return () => this.off(event, handler);
}
once(event: string, handler: Function) {
const wrapper = (...args: any[]) => {
handler(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
}
emit(event: string, ...args: any[]) {
this.events.get(event)?.forEach(fn => fn(...args));
}
off(event: string, handler: Function) {
this.events.get(event)?.delete(handler);
}
}
// 使用:跨组件通信
const bus = new EventBus();
bus.on('user:login', (user) => updateNavbar(user));
bus.emit('user:login', { name: 'Alice' });
策略模式
// 表单验证策略
type Validator = (value: string, rule: Rule) => string | null;
interface Rule {
type: string;
message: string;
min?: number;
max?: number;
pattern?: RegExp;
}
const strategies: Record<string, Validator> = {
required: (value) => value.trim() ? null : '此字段必填',
minLength: (value, rule) => value.length >= rule.min! ? null : rule.message,
maxLength: (value, rule) => value.length <= rule.max! ? null : rule.message,
pattern: (value, rule) => rule.pattern!.test(value) ? null : rule.message,
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? null : '邮箱格式不正确',
};
function validate(value: string, rules: Rule[]): string | null {
for (const rule of rules) {
const error = strategies[rule.type](value, rule);
if (error) return error;
}
return null;
}
// 使用
validate('ab', [
{ type: 'required', message: '' },
{ type: 'minLength', message: '至少3个字符', min: 3 },
]); // "至少3个字符"
单例模式
// 通用单例
function singleton<T>(creator: () => T): () => T {
let instance: T;
return () => {
if (!instance) instance = creator();
return instance;
};
}
// 使用
const getWebSocket = singleton(() => new ReconnectingWebSocket('wss://api.example.com/ws'));
const ws1 = getWebSocket();
const ws2 = getWebSocket();
console.log(ws1 === ws2); // true
// class 方式
class AppConfig {
private static instance: AppConfig;
private constructor(public readonly config: Record<string, string>) {}
static getInstance() {
if (!AppConfig.instance) {
AppConfig.instance = new AppConfig(loadConfig());
}
return AppConfig.instance;
}
}
代理模式
// 图片懒加载代理
class LazyImage {
private realImage: HTMLImageElement | null = null;
constructor(private placeholder: string, private src: string) {}
render(container: HTMLElement) {
const img = document.createElement('img');
img.src = this.placeholder; // 先显示占位图
img.dataset.src = this.src;
container.appendChild(img);
// IntersectionObserver 作为代理控制加载时机
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
img.src = this.src; // 进入视口才加载真实图片
observer.disconnect();
}
});
observer.observe(img);
}
}
// 数据缓存代理
function cacheProxy<T extends (...args: any[]) => Promise<any>>(fn: T): T {
const cache = new Map<string, any>();
return ((...args: any[]) => {
const key = JSON.stringify(args);
if (cache.has(key)) return Promise.resolve(cache.get(key));
return fn(...args).then(result => {
cache.set(key, result);
return result;
});
}) as T;
}
const fetchUser = cacheProxy(async (id: string) => {
const res = await fetch(`/api/users/${id}`);
return res.json();
});
// 第一次请求网络,第二次命中缓存
装饰器模式
// 函数装饰器:添加日志
function withLog<T extends (...args: any[]) => any>(fn: T): T {
return ((...args: any[]) => {
console.log(`调用 ${fn.name},参数:`, args);
const result = fn(...args);
console.log(`返回:`, result);
return result;
}) as T;
}
// 函数装饰器:添加防抖
function withDebounce(delay: number) {
return <T extends (...args: any[]) => any>(fn: T): T => {
let timer: ReturnType<typeof setTimeout>;
return ((...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
}) as T;
};
}
// 组合使用
const search = withDebounce(300)(
withLog(
(query: string) => fetchResults(query)
)
);
工厂模式
// 根据配置动态创建组件
type ComponentType = 'text' | 'number' | 'select' | 'date';
interface FieldConfig {
type: ComponentType;
label: string;
name: string;
options?: { label: string; value: string }[];
}
function createFormField(config: FieldConfig) {
switch (config.type) {
case 'text': return <TextInput {...config} />;
case 'number': return <NumberInput {...config} />;
case 'select': return <SelectInput {...config} options={config.options} />;
case 'date': return <DateInput {...config} />;
}
}
// 配置化渲染表单
const fields: FieldConfig[] = [
{ type: 'text', label: '姓名', name: 'name' },
{ type: 'select', label: '城市', name: 'city', options: [...] },
{ type: 'date', label: '生日', name: 'birthday' },
];
function DynamicForm() {
return (
<form>
{fields.map(field => (
<div key={field.name}>{createFormField(field)}</div>
))}
</form>
);
}
适配器模式
// 第三方 SDK 接口适配
// 旧接口
interface OldAnalytics {
trackEvent(category: string, action: string, label: string): void;
}
// 新接口
interface NewAnalytics {
send(event: string, data: Record<string, string>): void;
}
class AnalyticsAdapter implements NewAnalytics {
constructor(private old: OldAnalytics) {}
send(event: string, data: Record<string, string>) {
this.old.trackEvent(data.category || event, data.action || '', data.label || '');
}
}
// API 适配:后端返回格式统一
function adaptUserResponse(raw: any): User {
return {
id: raw.user_id ?? raw.id,
name: raw.user_name ?? raw.name,
avatar: raw.avatar_url ?? raw.avatar,
};
}
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 观察者内存泄漏 | 忘记取消订阅 | 返回 unsubscribe 函数,组件销毁时调用 |
| 单例测试困难 | 全局状态共享 | 依赖注入或提供 reset 方法 |
| 过度设计 | 简单问题套复杂模式 | 三行能解决的不要用模式 |
| 策略膨胀 | 策略类太多 | 按业务域拆分,或用 Map/对象替代 class |
最佳实践
- 先有问题再选模式,不为模式而模式
- 前端最常用:观察者/发布订阅、策略、代理、工厂
- 观察者必须提供取消订阅,避免内存泄漏
- 策略模式用对象/Map 即可,不必每个策略一个 class
- 适配器用于统一接口,不要在业务代码中直接适配
面试题
Q1: 观察者模式和发布订阅模式有什么区别?
核心区别在于是否有中间层:观察者模式中 Subject 直接通知 Observer(两者有直接依赖);发布订阅模式通过事件中心(EventBus)解耦,发布者和订阅者互不知道对方存在。观察者模式更直接,发布订阅模式更解耦。Vue 响应式系统是观察者模式,EventEmitter/Redux 是发布订阅模式。
Q2: 单例模式有哪些应用场景?
前端典型场景:1) 全局状态管理(Vuex/Pinia store);2) WebSocket 连接管理(确保只有一个连接);3) 全局配置/缓存;4) 浏览器
window/document本身就是单例。实现方式:闭包缓存实例、class 静态方法getInstance()、ES Module 顶层变量(模块天然单例)。
Q3: 策略模式有什么优势?
策略模式将算法封装为可互换的策略对象,优势:1) 消除大量
if/else或switch,用策略映射替代条件分支;2) 新增策略只需添加新对象,符合开闭原则;3) 策略可复用和独立测试。典型场景:表单验证规则、折扣计算、排序算法选择。前端常用对象/Map 存储策略,不必每个策略一个 class。
相关链接: