
在软件工程中,SOLID 原则是五个重要的面向对象设计原则,它们帮助开发者写出可维护、可扩展、易于理解的代码。这五个原则分别是:
-
单一职责原则(SRP):一个类应该只有一个职责。
-
开放封闭原则(OCP):软件实体应该对扩展开放,对修改封闭。
-
里氏替换原则(LSP):子类应该能够替代父类。
-
接口隔离原则(ISP):不应该强迫一个类依赖它不需要的接口。
-
依赖倒置原则(DIP):高层模块不应依赖低层模块,二者应依赖抽象。
这些原则鼓励我们将代码分割为单一职责的小模块,使得系统更加灵活,易于扩展和修改。而 设计模式 则为我们提供了一套经过验证的解决方案,帮助我们在开发过程中遵循这些原则。
在前端开发中,随着应用越来越复杂,业务逻辑和用户界面都需要更清晰、更易于维护的设计。因此,掌握设计模式不仅能帮助开发者组织代码,还能确保软件在迭代过程中具有良好的扩展性。设计模式通过提供标准的解决方案和架构框架,帮助我们更好地实现 SOLID 原则,尤其是在面对快速迭代和复杂业务场景时。
接下来,我们将深入探讨前端开发中常见的设计模式。
设计模式概述
前端开发已从简单的脚本编写演进为复杂的应用构建。面对日益繁重的业务逻辑与快速迭代的需求,如何写出结构清晰、易于扩展、便于协作的代码,成为每一位开发者必须面对的挑战。
设计模式 正是应对这一挑战的强大武器。它并非具体的代码,而是一套历经验证的最佳实践方案与设计思想,能为我们提供解决常见问题的可靠模板。
通常,设计模式可分为以下三类:
-
创建型模式:封装对象的创建过程,将系统与具体对象的实例化逻辑解耦。它们通过对创建机制进行抽象,有效避免硬编码带来的依赖问题,提升代码的可维护性与可测试性。
-
结构型模式:关注如何将类与对象组合成更大、更合理的结构,以提高系统的灵活性、可复用性和可组织性。它们通过识别对象间的关系来简化整体结构的设计。
-
行为型模式:识别对象之间的通信模式与职责分配,以提高系统组件间交互的灵活性与可扩展性。它们不仅定义对象之间的交互方式,还注重职责的合理分配。
设计模式详解
1. 创建型模式
工厂模式
工厂模式提供了一种创建对象的方式,而无需直接指定具体类的实例化过程。通过工厂模式,客户端代码无需关心对象的具体实现和实例化过程,只需要通过一个工厂方法获取所需的对象实例。
核心思想:
-
对象创建的封装:工厂模式将对象的创建逻辑封装在一个工厂类中,客户端通过工厂来获取对象,而不需要自己去负责实例化。
-
接口或抽象类的定义:工厂模式通常会定义一个产品的接口或抽象类,具体的实现类由工厂方法提供。工厂根据不同的条件(如参数、配置等)返回不同类型的产品对象。
你可以把它想象成一个真正的工厂。比如,你想买一辆车,你不需要知道这辆车是如何在生产线上组装出来的(不需要调用 new Car(...)并传入各种复杂的发动机、轮胎参数),你只需要告诉汽车工厂(工厂函数)你想要的型号(参数),工厂就会返回一辆完整的汽车给你(对象实例)。
这样做的主要目的是将对象的创建与使用分离,从而带来极高的灵活性和可维护性。
实际场景:
React.createElement
React.createElement 是 React 框架中最核心的 API 之一,它用于创建 虚拟 DOM(Virtual DOM)元素 。在使用 JSX 时,我们通常并不会显式调用它,但每一个 JSX 表达式最终都会被编译成 React.createElement 的调用。
tsx
const element = <div className="title">Hello</div>;
会被 Babel 编译为:
tsx
const element = React.createElement(
'div',
{ className: 'title' },
'Hello'
);
可以看出,React.createElement 是一个工厂函数 ,用于根据传入的类型(type)、配置(props)和子元素(children)生产出标准化的虚拟节点对象。这样的好处在于:
-
解耦创建与使用
React.createElement将虚拟 DOM 元素的创建过程封装起来,客户端只关心如何使用它,而不需要了解每个元素的具体实现。这种解耦减少了代码之间的依赖,使得组件创建与底层实现分离,增强了代码的灵活性。 -
单一职责原则(SRP)
React.createElement作为工厂函数,专注于生成虚拟 DOM 元素的任务。将这一逻辑集中到一个地方,简化了组件的创建过程,确保了代码的可维护性和扩展性。 -
开闭原则(OCP)
通过
React.createElement,我们可以在不修改现有代码的情况下,轻松地扩展新组件类型。只需在工厂函数中添加新的逻辑即可支持新的组件类型,避免了修改已有代码,符合开闭原则。
动态生成表单项
表单配置来自后端,不同的 type 要渲染不同的组件,我们可以用 工厂模式 动态创建:
tsx
// FormItemFactory.js
class InputComponent {
render(item) {
return `<input placeholder="请输入${item.label}" name="${item.field}" />`;
}
}
class SelectComponent {
render(item) {
const options = item.options.map(o => `<option>${o}</option>`).join('');
return `<select name="${item.field}">${options}</select>`;
}
}
class DateComponent {
render(item) {
return `<input type="date" name="${item.field}" />`;
}
}
export class FormItemFactory {
static create(item) {
switch (item.type) {
case 'input':
return new InputComponent();
case 'select':
return new SelectComponent();
case 'date':
return new DateComponent();
default:
throw new Error(`未知的表单类型: ${item.type}`);
}
}
}
// 使用
const formSchema = [
{ type: 'input', label: '用户名', field: 'username' },
{ type: 'select', label: '角色', field: 'role', options: ['管理员', '用户'] },
{ type: 'date', label: '注册时间', field: 'registerDate' },
];
const html = formSchema.map(item => {
const component = FormItemFactory.create(item);
return component.render(item);
}).join('');
document.body.innerHTML = `<form>${html}</form>`;
通过后端传回的Schema来判断当前需要渲染什么表单项,根据type创建对应的组件。
单例模式
单例模式确保一个类只有一个实例,并提供一个全局访问点来获取这个实例。换句话说,单例模式通过限制实例化的次数来确保系统中某个类仅有一个实例,且该实例会被共享,避免了对资源的浪费。
核心思想:
-
唯一实例:确保一个类只有一个实例。
-
全局访问点:通过一个全局的访问方法来访问该实例。
-
延迟加载:通常会采用延迟加载策略,只有在第一次需要该实例时,才会创建它。
实际场景:
单例模式的应用场景有很多,因为在 js 中我们可以直接生成对象,并且这个对象就是全局唯一,所以在 js中,单例模式是浑然天成的,我们平常经常不会感知到。比如Vuex、redux全局态管理,以及很多第三方库都是单例模式,多次引用只会使用同一个对象,比如jquery、moment 等等。
在前端状态管理中,Redux 的核心设计体现了单例模式的应用。Redux 通过全局唯一的 store 实例,实现了应用中状态的集中管理与统一访问。在典型的使用场景中,开发者通过 createStore 函数创建一个 store,并在模块中导出该实例,例如:
tsx
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer); // 创建全局唯一 store
export default store;
该 store 在模块加载时被初始化,并在整个应用生命周期内保持唯一性。Redux store 内部维护状态(state)、订阅者列表(listeners)以及 dispatch 方法,组件通过 Provider 注入 store,并使用 connect 或 useSelector 获取状态,实现状态在整个组件树中的共享。例如:
tsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
function Root() {
return (
<Provider store={store}>
<App />
</Provider>
);
}
Redux 使用单例模式 来确保全局唯一的 store 实例,带来了以下好处:
-
全局一致性 :通过唯一的
store实例,保证了应用中的状态一致性和可靠性,避免了多个实例之间的状态不同步问题。 -
集中化管理:状态的集中管理使得应用的状态更易于追踪、调试和维护,简化了开发和测试过程。
2. 结构型模式
代理模式
代理模式为某个对象提供一个代理对象,通过代理对象控制对真实对象的访问。
代理模式的核心思想是:为某个对象提供一个代理对象,用以控制对该对象的访问。代理对象在客户端与真实对象之间起中介作用,可以在访问真实对象之前或之后执行附加操作,如延迟加载、访问控制、缓存、日志记录、数据验证等。
代理模式的典型结构包括三类角色:
-
真实主题(Real Subject):被代理的实际对象,包含核心业务逻辑。
-
代理(Proxy):对真实对象的引用,对外提供相同的接口,并在调用前后附加额外逻辑。
-
客户端(Client):通过代理间接访问真实对象,而无需关心内部实现
实际场景:
Vue3的响应式系统
在现代前端框架中,代理模式被广泛用于数据拦截、响应式系统和懒加载机制 中。特别是在 Vue 3 的响应式系统中,Proxy 对象取代了 Vue 2 中的 Object.defineProperty(),成为核心实现方式。通过代理模式,Vue 可以在属性被访问(get)或修改(set)时自动触发依赖收集与视图更新逻辑,从而实现数据的自动响应式绑定。

Vue 3 的响应式系统核心由 reactive() 方法实现,其内部使用 ES6 的 Proxy 对目标对象进行拦截与包装:
tsx
// 简化后的 Vue 3 响应式核心实现
const handler = {
get(target, key, receiver) {
// 依赖收集
track(target, key);
const res = Reflect.get(target, key, receiver);
return typeof res === 'object' && res !== null ? reactive(res) : res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
// 派发更新
trigger(target, key);
}
return result;
}
};
function reactive(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
return new Proxy(target, handler);
};
在该实现中,Proxy 充当了代理对象 ,拦截对真实数据对象的读写操作;get 拦截器负责依赖收集(追踪哪些组件依赖该数据);set 拦截器负责派发更新(在数据变化时通知相关组件重新渲染)。
在 Vue 3 的响应式实现中,代理模式带来了以下三大好处:
-
控制访问 :
Proxy可以透明地拦截并控制对数据的访问,使得依赖收集和视图更新等操作不需要显式管理。 -
增强功能 :通过代理,
Proxy为数据对象增强了功能(如自动更新视图、收集依赖等),而无需修改原数据结构。 -
透明化操作:开发者无需关心代理的内部实现,代理自动处理数据操作的复杂性,简化了开发过程。
函数缓存
在实际开发中,有些函数的计算过程较为耗时或频繁调用,例如复杂的计算、数据处理或远程请求等。
为了避免重复计算、提高程序性能,我们可以利用代理模式为原函数创建一个带有缓存功能的"代理函数"。
tsx
// 代理模式实现函数结果缓存
function createMemoizedFunction(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
// 使用 JSON 序列化作为缓存键,更可靠地区分不同参数组合
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('从缓存中读取结果');
return cache.get(key);
}
console.log('计算结果并缓存');
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
}
});
}
// 示例函数:计算两个数的和
function add(a, b) {
console.log('执行原始函数');
return a + b;
}
// 创建带缓存功能的代理函数
const memoizedAdd = createMemoizedFunction(add);
// 使用示例
console.log(memoizedAdd(2, 3)); // 计算结果并缓存
console.log(memoizedAdd(2, 3)); // 从缓存中读取结果
console.log(memoizedAdd(4, 5)); // 计算结果并缓存
当被代理的目标是函数(或可调用对象)时,Proxy 提供了 apply 来专门拦截函数调用。
Lodash 提供的 _.memoize 本质上与上面的逻辑一致,它内部也是通过 缓存参数 → 返回结果 的方式实现的。
装饰者模式
装饰者模式是一种结构型设计模式,它允许你动态地给一个对象添加额外的功能,而不改变其结构。装饰者模式通过创建一个包装对象,来对已有对象进行扩展,增强其功能。
核心思想:
-
装饰者通过包装(包裹)原始对象,增强或修改其行为,而不需要改变对象的代码。
-
装饰者是对象的一种外部包裹,它不改变被装饰对象的内部结构,可以动态地给对象添加新功能。
-
装饰者模式通常与继承方式相比,更具灵活性,可以实现更复杂的行为组合。
实际场景:
高阶组件
高阶组件(HOC) 是 React 中的一种模式,接受一个组件作为参数,返回一个增强版的组件。高阶组件主要用于 代码重用 和 跨组件功能的增强。

高阶组件的工作原理:
-
包装:HOC 接受一个组件作为输入,返回一个新组件,这个新组件通常会增强原组件的功能,比如添加额外的 props、状态管理、生命周期钩子等。
-
增强功能:HOC 可以动态地向组件添加额外的功能,比如条件渲染、权限校验、数据加载等,而不修改原组件的代码。
tsx
function withLoading(Component) {
return function WithLoading(props) {
if (props.isLoading) {
return <div>Loading...</div>;
}
return <Component {...props} />;
};
}
function withErrorHandling(Component) {
return function WithErrorHandling(props) {
if (props.hasError) {
return <div>Error occurred!</div>;
}
return <Component {...props} />;
};
}
// 原始组件
function MyComponent({ data }) {
return <div>{data}</div>;
}
// 组合多个高阶组件
const EnhancedComponent = withLoading(withErrorHandling(MyComponent));
// 使用组合后的组件
function App() {
return <EnhancedComponent isLoading={false} hasError={true} data="Hello" />;
}
这里,MyComponent 通过 withLoading 和 withErrorHandling 两个高阶组件组合,分别增强了 加载状态 和 错误处理 功能,这种组合方式就像是装饰者模式中的多个装饰器增强了同一个对象的功能。
高阶组件 之所以使用 装饰者模式,是因为:
-
动态增强功能:可以在不修改原始组件的情况下,动态地为其添加新的功能。
-
单一职责原则(SRP):每个高阶组件专注于增强组件的某一功能,使得代码更加模块化,易于维护。
-
可组合性和复用性:多个高阶组件可以组合使用,使得功能增强变得更加灵活和可复用。
自动增加请求时的 loading 提示
在实际开发中,我们常常需要在发送 Axios 请求 时显示 loading 提示,并在请求完成后隐藏。使用 装饰者模式 可以很方便地为 Axios 请求方法添加这类行为,而不需要修改原始的请求逻辑。
tsx
// loading装饰器
function withLoading(fn: Function): Function {
return async function (...args: any[]) {
// 在请求开始前显示 loading
console.log("Loading..."); // 可以用一个 UI 元素来替代这里的 console.log
try {
const response = await fn(...args); // 执行原始的 Axios 请求
return response;
} catch (error) {
console.error(error);
throw error;
} finally {
// 请求结束后隐藏 loading
console.log("Loading finished."); // 同样也可以用关闭 UI 元素来替代这里的 console.log
}
};
}
// 封装的 Axios 请求
function axiosRequest(config: any) {
return axios(config);
}
// 使用装饰器增强 Axios 请求
const axiosWithLoading = withLoading(axiosRequest);
通过 withLoading 装饰器增强 axiosRequest,生成新的方法 axiosWithLoading。该方法带有自动显示和隐藏 loading 的功能。
3. 行为模式
策略模式
策略模式允许你在运行时选择不同的算法或行为,而不修改使用算法的对象。策略模式通过将算法封装成独立的策略类,使得它们可以互相替换,从而达到动态选择算法或行为的目的。
策略模式的核心思想是:定义一系列算法(策略),将每个算法封装起来,使它们可以相互替换,而不影响使用这些算法的客户端。策略模式使得算法的变化独立于使用算法的对象,从而实现代码的可扩展性与可维护性。
策略模式所封装的算法都是在完成相同的工作,只是实现方式不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。策略模式的优点是简化了单元测试,每个算法都有自己的类,因此可以单独编写接口测试,比较方便。
在软件设计中,策略模式通常由以下三个部分组成:
-
上下文(Context):持有对某个策略对象的引用,并在运行时动态地选择策略;
-
策略接口(Strategy):定义算法族的公共接口;
-
具体策略(ConcreteStrategy):实现具体的算法逻辑。
通过这种结构,程序可以在运行时灵活切换算法或行为,而无需修改调用方代码。
实际场景:
路由系统模式
在前端领域,路由系统(Router) 是策略模式的典型应用场景。以 Vue Router3 为例,现代路由系统通常支持多种路由模式,例如 Hash 模式 ,History 模式 和Abstract模式。三者的核心逻辑不同,但对外提供统一的接口, Vue Router会根据用户选择的模式匹配相应的路由替换规则,这正是策略模式的体现。
tsx
// Vue Router3源码(简化版)
import { HashHistory } from "./history/hash";
import { HTML5History } from "./history/html5";
import { AbstractHistory } from "./history/abstract";
export default class VueRouter {
constructor(options: RouterOptions = {}) {
let mode = options.mode || "hash";
this.fallback =
mode === "history" && !supportsPushState && options.fallback !== false;
if (this.fallback) {
mode = "hash";
}
if (!inBrowser) {
mode = "abstract";
}
this.mode = mode;
switch (mode) {
case "history":
this.history = new HTML5History(this, options.base);
break;
case "hash":
this.history = new HashHistory(this, options.base, this.fallback);
break;
case "abstract":
this.history = new AbstractHistory(this, options.base);
break;
default:
if (process.env.NODE_ENV !== "production") {
assert(false, `invalid mode: ${mode}`);
}
}
}
}
好处:
-
解耦不同的路由策略 :策略模式将不同的路由模式(如
Hash模式、History模式、Abstract模式)封装在独立的策略类中,这样每种路由模式的实现是隔离的,彼此之间没有紧密耦合。使用策略模式后,用户可以在不改变现有代码的情况下,选择和切换不同的路由策略。 -
易于扩展:当需要支持新的路由模式时,只需创建新的策略类并添加到系统中,而无需修改现有的代码。这使得 Vue Router 在未来添加新功能时变得更加灵活,增强了系统的可扩展性。
uni-app跨平台
在 uni-app 中,通过策略模式完成多端适配是非常常见的需求,对于不同平台,事件的触发机制可能会有所不同。在 uni-app 中,可以通过条件判断或动态绑定的方式来选择不同的事件处理策略。
tsx
<template>
<view @tap="handleTap" class="clickable">
<text>Click me</text>
</view>
</template>
<script>
export default {
methods: {
handleTap() {
// iOS、Android 和 H5 可能需要不同的事件策略
if (uni.getSystemInfoSync().platform === 'ios') {
this.handleIosTap();
} else if (uni.getSystemInfoSync().platform === 'android') {
this.handleAndroidTap();
} else {
this.handleWebTap();
}
},
handleIosTap() {
console.log('iOS-specific tap handling');
// iOS 特有的处理逻辑
},
handleAndroidTap() {
console.log('Android-specific tap handling');
// Android 特有的处理逻辑
},
handleWebTap() {
console.log('Web-specific tap handling');
// Web 特有的处理逻辑
}
}
}
</script>
uni-app 提供了 uni.getSystemInfoSync() 方法来获取设备的系统信息。在这个示例中,handleTap 方法根据不同平台选择了不同的事件处理策略。这种基于条件判断选择不同的处理逻辑也是策略模式的应用。
迭代器模式
迭代器模式让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。
其核心思想是:为一个聚合对象提供一种顺序访问其元素的方法,而又不暴露该对象的内部表示。用更通俗的话说,就是把"按某种顺序遍历集合"的责任交给一个独立的迭代器对象,客户端只通过迭代器访问元素,而不用关心集合内部是数组、树还是其它结构。同时,通过引入迭代器,集合的遍历逻辑与集合的存储结构解耦,方便维护和扩展。
实际场景
在前端领域,迭代器模式的思想与 ES6 引入的可迭代协议 与 迭代器协议 高度契合。通过在对象上定义 Symbol.iterator 方法,我们即可使任意对象具备可迭代能力(即能被 for...of、Array.from()、扩展运算符等语法结构遍历)。

下面通过 Symbol.iterator 自定义一个可迭代对象 Range,实现对数值区间的遍历。
tsx
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { done: true };
}
}
};
}
};
// 使用:可直接用 for...of 遍历
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
-
range对象通过定义[Symbol.iterator](),成为一个可迭代聚合对象; -
返回的对象拥有
next()方法,是具体的迭代器; -
for...of语法糖自动调用[Symbol.iterator]()并反复执行next(),直到done: true。
观察者模式
观察者模式允许一个对象(被观察者)在其状态变化时通知多个依赖于它的对象(观察者)。通过这种方式,观察者无需直接依赖被观察者的内部实现,系统的耦合性降低,扩展性增强。
核心思想:
-
被观察者(Subject):持有一系列观察者对象,并提供方法让观察者进行注册、注销。
-
观察者(Observer):每个观察者都依赖于被观察者,当被观察者状态改变时,所有已注册的观察者都会被通知并作出响应。
-
通知机制:被观察者状态变化时,会自动通知所有已注册的观察者,通常通过调用观察者的更新方法来实现。

实际场景:
Vue2响应式系统
Vue 2 的响应式原理就是观察者模式的典型实现。通过 Object.defineProperty() 拦截对象属性的读写操作,实现数据驱动视图更新。其核心由三个部分组成:
-
Observer :将对象的每个属性转化为响应式的,在
getter中进行依赖收集,在setter中派发更新; -
Dep:依赖收集器,维护一组观察者(Watcher),当属性变化时通知它们执行更新;
-
Watcher:观察者对象,订阅数据变化并触发回调或组件重新渲染。
tsx
// 定义响应式属性
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
if (Dep.target) dep.depend(); // 依赖收集
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify(); // 通知观察者更新
}
},
});
}
// 依赖管理器
class Dep {
constructor() {
this.subs = [];
}
depend() {
if (Dep.target) this.subs.push(Dep.target);
}
notify() {
this.subs.forEach(sub => sub.update());
}
}
当组件渲染时,Watcher 读取数据属性触发 getter,从而被 Dep 收集为依赖;当数据修改时,setter 调用 dep.notify(),触发所有相关观察者更新,实现数据与视图的联动。
使用观察者模式是因为 Vue 的响应式场景正好符合它的特点:数据变化驱动多方更新、依赖动态且数量不固定、数据与视图需解耦。其他模式(如策略、工厂、代理、中介)无法同时满足这些需求,而观察者模式天然支持一对多通知、动态依赖收集和统一更新机制。
表单字段监听
一个字段控制另一个字段的显隐状态(比如,当用户选择一个选项或输入某个值时,另一个字段显示或隐藏),可以通过 观察者模式 来实现这一功能。
tsx
class Form {
constructor() {
this.fields = [];
}
addField(field) {
this.fields.push(field);
}
// 通知所有字段进行更新
notify() {
this.fields.forEach(field => field.update());
}
}
class Field {
constructor(name, value = '') {
this.name = name;
this.value = value;
this.form = null;
this.observer = null; // 用来保存监听的字段
}
setForm(form) {
this.form = form;
}
setValue(value) {
this.value = value;
this.form.notify(); // 当字段值变化时通知表单更新
}
// 设置观察者,监听另一个字段的变化
setObserver(observer) {
this.observer = observer;
}
// 用于更新字段的显示状态或其他行为
update() {
// 默认的 update 行为可以为空,子类可以重写
}
}
class VisibilityField extends Field {
constructor(name, value = '') {
super(name, value);
this.isVisible = true; // 默认显示
}
update() {
if (this.observer) {
this.isVisible = this.observer.value === 'yes'; // 控制显示与隐藏
}
console.log(`${this.name} field is now ${this.isVisible ? 'visible' : 'hidden'}`);
}
}
// 使用示例
const form = new Form();
// 创建字段:是否需要发票 和 发票抬头
const invoiceRequired = new Field('Invoice Required', 'no');
const invoiceTitle = new VisibilityField('Invoice Title');
// 设置字段之间的依赖关系,发票抬头字段监听是否需要发票字段
invoiceTitle.setObserver(invoiceRequired);
// 将字段添加到表单
form.addField(invoiceRequired);
form.addField(invoiceTitle);
// 模拟用户选择:是否需要发票
invoiceRequired.setValue('yes'); // 显示发票抬头字段
invoiceRequired.setValue('no'); // 隐藏发票抬头字段
invoiceTitle 字段监听 invoiceRequired 字段的值。如果 invoiceRequired 的值为 yes,invoiceTitle 字段显示;如果为 no,则隐藏。每次 invoiceRequired 字段的值变化时,invoiceTitle 字段的 update 方法会被调用,从而更新它的显示状态。
中介者模式
中介者模式 通过引入一个中介者对象来封装对象之间的交互,使各对象不再直接引用彼此,从而降低系统的耦合度。
核心思想:
-
中介者(Mediator):负责协调各个同事对象的交互逻辑,成为通信的中心;
-
同事对象(Colleague):不直接与其他同事对象通信,而是通过中介者发送和接收消息。
这种模式可以把复杂对象网络的多对多关系,转化为中心化的中介控制,便于维护和扩展。
实际场景:
在 Redux 中,store 的作用非常类似于中介者模式中的 Mediator 。它充当了状态管理的中心,并协调所有组件与应用状态之间的交互。组件不直接改变状态,而是通过发送 actions 来通知 store 进行状态更新,组件则通过 connect 来订阅状态变化,从而避免了组件间的直接依赖。
Redux 的中介者模式:
-
store 作为中介者,所有组件的状态变化都通过它来进行。
-
组件不直接修改状态,而是通过 actions 和 reducers 来请求状态变化,确保状态管理的集中化。
tsx
import { createStore } from 'redux';
// Action 类型
const UPDATE_MESSAGE = 'UPDATE_MESSAGE';
// 初始状态
const initialState = { message: '' };
// Reducer:根据 action 更新 state
const messageReducer = (state = initialState, action) => {
switch (action.type) {
case UPDATE_MESSAGE:
return { ...state, message: action.payload };
default:
return state;
}
};
// 创建 Redux store,store 作为中介者管理所有的状态变更
const store = createStore(messageReducer);
// Action 创建函数
export const updateMessage = (message) => ({
type: UPDATE_MESSAGE,
payload: message,
});
export default store;
在 Redux 中,组件和状态之间的关系可以被看作是 多对多 的。在传统的多对多关系中,多个组件之间可能需要直接交互,导致 组件之间互相引用,耦合度高,依赖混乱 。而通过引入 store 作为中介者,所有的状态变化都变成了集中管理 ,中介者模式非常适合 组件众多、状态多变的前端应用,是 Redux 设计的核心思想。
访问者模式
访问者模式允许你在不改变类的结构的前提下,定义作用于这些类的新操作。简单来说,访问者模式将操作与数据结构分离,使得你可以在不修改对象结构的情况下,为这些对象添加新的操作。
核心思想:
访问者模式的核心思想是:将操作的逻辑与数据结构解耦。通过引入一个新的访问者类,你可以针对不同的数据结构执行不同的操作,而不需要修改数据结构本身。
-
元素(Element):被访问的对象,通常是一些需要访问的对象结构中的元素。
-
访问者(Visitor) :定义了一组针对不同类型元素的操作。每个访问者实现一个
visit方法,用于访问不同的元素。 -
具体元素(ConcreteElement) :实现了
Element接口的具体对象。 -
具体访问者(ConcreteVisitor) :实现了
Visitor接口的具体访问者,定义了不同的操作。
实际场景:
在 React Router 中,Route 组件通常需要渲染某些组件,特别是在动态路由下,往往会传递不同的 props 给每个路由组件。**Route** 的渲染方法本质上也可以理解为访问者模式的一种实现。
tsx
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = () => <h2>Home Page</h2>;
const About = () => <h2>About Page</h2>;
const App = () => (
<Router>
<Switch>
{/* 访问者模式:Route 是"访问者",它渲染与路径匹配的组件 */}
<Route path="/" exact render={() => <Home />} />
<Route path="/about" render={() => <About />} />
</Switch>
</Router>
);
export default App;
-
Route组件充当了 访问者(Visitor) 的角色,它根据不同的 URL 路径动态地渲染不同的组件(如Home和About)。 -
这里,
Route在访问 URL 时并不改变组件本身的结构,而是 "访问"路径 并 执行渲染操作。 -
这种机制类似于访问者模式中的
**Visitor**,通过render或children来对每个匹配的组件执行特定的渲染操作,而不改变组件的内部结构。
React Router 使用访问者模式,是为了在不改变组件结构的前提下,通过统一访问接口实现动态渲染,使逻辑与视图解耦,增强系统的可扩展性与可维护性。
职责链模式
职责链模式 使得多个对象有机会处理请求,从而避免请求的发送者与接收者之间的直接耦合。职责链模式将多个处理对象连接成一条链,并沿着这条链传递请求,直到有一个对象处理它。
核心思想:
-
将多个处理请求的对象连接成一条链,形成链式结构。
-
请求沿着链传递,直到链上的某个处理对象能够处理它。
-
每个处理对象决定是否将请求传递给下一个处理对象。
实际场景:
axios的拦截器
在前端中,我们可能每天都在使用Promise,Promise就是基于职责链模式设计的,而我们给then方法中部署回调函数则就是每个职责节点的处理器,因为then方法返回一个新的Promise可以为其委托新的业务处理节点。

axios的拦截器管道就是基于Promise进行设计的,具体源码节选如下:
tsx
// 以省略无关代码
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 注册两个拦截器的管理器
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
}
Axios.prototype.request = function request(config) {
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
// 处理请求拦截器
this.interceptors.request.forEach(function unshiftRequestInterceptors(
interceptor
) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 处理响应拦截器
this.interceptors.response.forEach(function pushResponseInterceptors(
interceptor
) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 部署一个Promise链,类似链表的头插法操作,像多米罗骨牌那样的链式反应,是反向部署的
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
// 返回最开头的那个Promise给外界,到时候就可以起到导火索的作用
return promise;
};
-
request拦截器链和response拦截器链作为两个独立的链,将请求/响应的处理逻辑分别分隔开。 -
请求(或响应)会沿着链传递,每个拦截器都可以修改请求或响应数据,或者将其传递到下一个拦截器。
-
通过
Promise和拦截器,发送者(请求发起者)和接收者(拦截器、响应处理者)之间没有直接耦合。请求发起后,拦截器按顺序处理,最终返回处理结果。
Axios 的拦截器系统中,每个拦截器都承担特定的职责(如添加 Token、打印日志、统一错误处理等),它们既相互独立又需按顺序协作。职责链模式完美解决了这种"多步骤、解耦"的需求,并且可以随时添加、移除或调整拦截器顺序,而无需修改 Axios 核心逻辑,符合开闭原则(OCP)。
表单验证
在表单验证的场景中,可以通过职责链模式来逐步验证表单字段的有效性。例如,我们可以创建一个表单验证系统,通过职责链来依次验证表单中的各个字段。
tsx
// 验证器接口
class Validator {
constructor(next = null) {
this.next = next; // 下一个验证器
}
// 验证方法
validate(formData) {
if (this.next) {
return this.next.validate(formData);
}
return true; // 默认返回通过验证
}
}
// 用户名验证器
class UsernameValidator extends Validator {
validate(formData) {
if (!formData.username) {
console.log('用户名不能为空');
return false;
}
if (!/^[a-zA-Z0-9_]{3,20}$/.test(formData.username)) {
console.log('用户名必须是3到20个字母、数字或下划线');
return false;
}
return super.validate(formData); // 调用下一个验证器
}
}
// 密码验证器
class PasswordValidator extends Validator {
validate(formData) {
if (!formData.password) {
console.log('密码不能为空');
return false;
}
if (formData.password.length < 6) {
console.log('密码必须至少6个字符');
return false;
}
return super.validate(formData); // 调用下一个验证器
}
}
// 邮箱验证器
class EmailValidator extends Validator {
validate(formData) {
if (!formData.email) {
console.log('邮箱不能为空');
return false;
}
if (!/\S+@\S+\.\S+/.test(formData.email)) {
console.log('请输入有效的邮箱地址');
return false;
}
return super.validate(formData); // 调用下一个验证器
}
}
// 模拟表单数据
const formData = {
username: 'john_doe',
password: '123456',
email: 'john.doe@example.com'
};
// 创建验证器链
const validatorChain = new UsernameValidator(
new PasswordValidator(
new EmailValidator()
)
);
// 执行表单验证
const isValid = validatorChain.validate(formData);
console.log(`表单验证结果: ${isValid ? '通过' : '失败'}`);
Validator 是一个抽象类,定义了一个 validate 方法。每个具体的验证器会继承自 Validator,并重写 validate 方法,执行特定的验证逻辑。每个验证器在完成自己的验证后,会调用 super.validate(formData) 将请求传递给下一个验证器。最后,调用 validatorChain.validate(formData) 来执行整个验证链。
总结
通过对前端设计模式的讲解,我们发现这些模式不仅仅是一些抽象的设计思想,它们在实际应用中具有极高的实用性,能够帮助我们解决复杂业务中的各种挑战。在前端开发中,掌握设计模式不仅能提升我们的编码效率和系统的健壮性,还能增强团队协作能力,尤其是在面对不断扩展的项目时。设计模式帮助我们解耦系统组件,增强可复用性,确保系统的稳定和可测试性。
总的来说,设计模式在前端开发中是不可或缺的工具,它们帮助我们构建高质量的应用,提升开发和维护的效率,确保系统在快速迭代过程中保持稳定和可扩展。