设计模式

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/elseswitch,用策略映射替代条件分支;2) 新增策略只需添加新对象,符合开闭原则;3) 策略可复用和独立测试。典型场景:表单验证规则、折扣计算、排序算法选择。前端常用对象/Map 存储策略,不必每个策略一个 class。


相关链接: