责任链模式是前端架构中最经典的"链式处理"思想。它不像观察者或策略模式那样频繁出现在业务代码中,但一旦你理解了它,就会发现 Redux 中间件、Express 中间件、DOM 事件冒泡、甚至表单校验都可以用同一个模型来解释。
责任链模式的核心思想是:将多个处理者连接成一条链,请求沿着链依次传递,直到某个处理者处理它或链走完。 在前端开发中,它特别适合拆分"胖逻辑"------比如电商商品价格计算,往往涉及会员折扣、优惠券、满减、新人优惠等层层叠加的规则。本文从一个真实的电商价格计算场景出发,带你走进前端开发中的责任链模式。

一、一个"胖"价格计算函数
假设有一个商品价格计算的需求:
- 计算基础总价:
单价 × 数量 - 使用优惠券:满 200 减 30
- 会员折扣:普通会员 9.5 折,高级会员 9 折
最直接的实现方式是把所有规则塞进一个函数里:
javascript
function calculatePrice(price, quantity, coupon, memberLevel) {
// 1. 基础总价
let total = price * quantity;
// 2. 使用优惠券
if (coupon === '满200减30' && total >= 200) {
total -= 30;
}
// 3. 会员折扣
if (memberLevel === 'vip') {
total *= 0.9;
} else if (memberLevel === 'normal') {
total *= 0.95;
}
return total;
}
目前只有两条规则,函数还算看得过去。但产品很快会加新需求:长租优惠 (租期超过 6 个月打 9 折)、新人满减 、限时促销......
每加一条规则,都要在这个函数里继续塞 if-else。很快,calculatePrice 就会膨胀成一个几百行的"胖函数",难以阅读、难以测试、牵一发而动全身。
责任链模式正是为了解决这个问题:把每条计算规则拆成一个独立的处理节点,用链把它们串起来。每个节点只关心自己的逻辑,处理完后把结果传递给下一个节点。
你可能会觉得这和策略模式很像------都是把一堆逻辑拆成独立单元。但它们的核心区别在于:
- 策略模式是"选一个" :多个算法并列,调用方根据条件选择其中一个执行。比如支付方式------用户选微信就调微信支付,选支付宝就调支付宝支付,两者不会同时执行。
- 责任链模式是"走流程" :多个处理步骤串行,请求依次经过每个处理者。比如价格计算------一个商品可能同时享受长租优惠、优惠券和会员折扣,这些规则是叠加的,不是互斥的。
策略是"并联选择",责任链是"串联加工"。 价格计算中,各种优惠规则需要层层叠加,所以用责任链;支付渠道选择中,同一笔订单只需要一种支付方式,所以用策略。
二、用函数组合实现责任链
责任链模式的核心动作是:依次调用函数,每个函数接收上一个函数的输出,处理后再传给下一个。 这和 reduce 的行为完全吻合。
1. 定义每条规则为纯函数
javascript
// 每个规则函数签名为: (prevTotal, context) => newTotal
// prevTotal:上一步计算的结果
// context:计算所需的上下文信息(数量、会员等级等)
// 规则1:计算基础总价
function basePrice(prevTotal, { price, quantity }) {
return price * quantity;
}
// 规则2:优惠券(满200减30)
function couponRule(total, { coupon }) {
if (coupon === '满200减30' && total >= 200) {
return total - 30;
}
return total;
}
// 规则3:会员折扣
function memberDiscount(total, { memberLevel }) {
if (memberLevel === 'vip') return total * 0.9;
if (memberLevel === 'normal') return total * 0.95;
return total;
}
每条规则都是一个无副作用的纯函数,只做一件事,可以独立测试、独立修改。
2. 组合器:把规则函数串成链
javascript
// 责任链组合器:依次执行 rules,每个 rule 接收上一个的返回值
function createPriceChain(...rules) {
return (context) => {
return rules.reduce((total, rule) => rule(total, context), 0);
};
}
这里 reduce 的初始值是 0,第一条规则 basePrice 会基于 context 计算出真正的初始价格。
3. 使用
javascript
const calculate = createPriceChain(basePrice, couponRule, memberDiscount);
const result = calculate({
price: 100,
quantity: 3,
coupon: '满200减30',
memberLevel: 'vip',
});
console.log(result); // (100*3 - 30) * 0.9 = 243
新增一条"长租优惠"规则,只需要新增一个函数,插入到链中即可,完全不用修改已有规则:
javascript
// 新增长租优惠规则:租期 >= 6 个月,打 9 折
function longTermDiscount(total, { months }) {
if (months >= 6) return total * 0.9;
return total;
}
// 新链:基础价 → 长租优惠 → 优惠券 → 会员折扣
const calculateV2 = createPriceChain(
basePrice,
longTermDiscount, // 插入新规则
couponRule,
memberDiscount
);
这正是开闭原则的体现------对扩展开放,对修改封闭。
三、封装成自定义 Hook:usePriceCalculator
在实际项目中,计算逻辑通常放在组件内,而规则链的配置可能来自后端或全局配置。我们可以封装一个 usePriceCalculator Hook,让组件直接用。
typescript
// hooks/usePriceCalculator.ts
import { useMemo } from 'react';
type PriceRule = (total: number, context: Record<string, any>) => number;
export function usePriceCalculator(rules: PriceRule[]) {
// 使用 useMemo 缓存组合后的计算函数,规则数组不变则不重新创建
const calculate = useMemo(() => {
return (context: Record<string, any>) => {
return rules.reduce((total, rule) => rule(total, context), 0);
};
}, [rules]);
return { calculate };
}
在 React 组件中使用
tsx
// components/PriceDisplay.tsx
import { usePriceCalculator } from '@/hooks/usePriceCalculator';
// 导入或定义规则函数(可来自配置文件)
const rules = [basePrice, longTermDiscount, couponRule, memberDiscount];
function PriceDisplay({ product, quantity, coupon, memberLevel, months }) {
const { calculate } = usePriceCalculator(rules);
const totalPrice = calculate({
price: product.price,
quantity,
coupon,
memberLevel,
months,
});
return <div>最终价格:¥{totalPrice.toFixed(2)}</div>;
}
如果规则是动态的(比如不同活动页使用不同规则),只需传入不同的 rules 数组即可:
tsx
// 普通活动页
const normalRules = [basePrice, couponRule, memberDiscount];
// 长租活动页
const longTermRules = [basePrice, longTermDiscount, couponRule, memberDiscount];
function ActivityPage({ isLongTerm }) {
const { calculate } = usePriceCalculator(isLongTerm ? longTermRules : normalRules);
// ...
}
四、为什么这个 Hook 是责任链模式?
- 多个处理者(规则函数)连接成一条链 :
rules数组就是链。 - 请求(context)沿着链依次传递 :
reduce遍历每个规则,将上一次的结果传给下一个规则。 - 某个规则可以"处理"并修改结果 :每个规则都可以基于
total和context返回新的total,也可以选择不做任何修改(返回原值)。 - 发送者(组件)与接收者(规则函数)解耦 :组件只负责调用
calculate(context),不关心内部有哪些规则、顺序如何。
五、进阶:支持"短路"的责任链
有时候我们需要某个规则在满足条件后截断链,不再执行后续规则(比如"限时秒杀价"直接覆盖所有规则)。可以在规则函数中返回一个特殊标记。
javascript
const STOP = Symbol('stop');
function flashSaleRule(total, { isFlashSale, flashPrice }) {
if (isFlashSale) {
return flashPrice; // 直接返回秒杀价,不执行后续规则
}
return total;
}
修改组合器,让 reduce 在遇到 STOP 时跳出:
javascript
function createPriceChain(...rules) {
return (context) => {
let total = 0;
for (const rule of rules) {
total = rule(total, context);
// 如果规则返回了特殊标记,停止后续规则
if (total === STOP) break;
}
return total;
};
}
不过,对于价格计算场景,通常不需要截断,每个规则都应该有序叠加。这种"短路"机制更适用于表单校验链(遇到第一个错误就停)。
六、总结
责任链模式的核心思想是链式传递,依次处理,它并不依赖于类。在函数式编程中,我们可以用以下方式实现:
| 实现方式 | 适用场景 |
|---|---|
reduce + 规则函数数组 |
数据依次变换(价格计算、数据清洗) |
for...of + 短路返回 |
校验链、权限检查链 |
| 自定义 Hook 封装 | React/Vue 组件中复用责任链逻辑 |
useReducer + 策略对象 |
状态管理中的分发链 |
useReducer本身就是一种"内置的责任链"。每个case是一个处理者,action沿着switch链依次匹配,匹配到就处理并停止。
责任链模式和粗略模式的区别,可以用一个简单的类比来区分:
- 策略模式 :你去餐厅吃饭,菜单上有川菜、粤菜、日料。你主动选择一种菜系,厨房只按这种菜系的做法给你做菜。各菜系之间互不干扰。
- 责任链模式 :你去医院看病,先挂号,然后去诊室,医生开了药,你去药房取药。你的"请求"依次经过多个窗口,每个窗口处理自己的那部分,然后交棒给下一个窗口。你不需要选择走哪条路------流程是固定的。
当你发现代码里出现一连串的 if-else 或嵌套的 try-catch,且每个分支都判断"是否能处理"时,就是责任链模式该上场的时候。