Vue 组件解耦实践:用回调函数模式替代枚举类型传递
前言
在 Vue 组件开发中,父子组件通信是一个常见场景。当子组件需要触发父组件的某个操作,而父组件又需要根据触发来源执行不同逻辑时,很容易写出耦合度较高的代码。本文通过一个真实的登录模块重构案例,介绍如何使用回调函数模式来解耦组件。
问题场景
业务背景
在登录页面中,验证码登录组件有两个操作入口:
-
点击"获取验证码"按钮
-
点击"登录"按钮
两个操作都需要检查用户是否同意服务协议。如果未同意,需要弹出协议确认弹窗。用户确认后,根据触发来源执行不同的后续操作。
原有实现
typescript
// codeLogin.enum.ts - 子组件定义枚举
export const CodeLoginEnum = {
CODE_BTN: 'code-btn', // 获取验证码按钮
LOGIN_BTN: 'login-btn' // 登录按钮
} as const;
// codeLogin.vue - 子组件
const getCode = () => {
if (!isAgree.value) {
emit('changeCodeAgreeDisplayType', CodeLoginEnum.CODE_BTN); // 告诉父组件是哪个按钮
emit('toggleAgreeDialog', true);
return;
}
// ...
}
// login.vue - 父组件
const handleAgreementConfirm = () => {
if (codeAgreeDisplayType.value === CodeLoginEnum.LOGIN_BTN) {
// 登录按钮触发的,需要校验验证码
if (!verifyKey.value) {
ElMessage.warning('请先获取验证码');
return;
}
}
codeLoginInstance.value?.doGetCode();
}
问题分析
-
父组件依赖子组件内部细节 :父组件需要导入并理解
CodeLoginEnum -
违反开闭原则:子组件新增按钮时,父组件也需要修改
-
职责不清:子组件的业务逻辑分散在父子两个组件中
-
可测试性差:父组件的逻辑依赖子组件的枚举定义
解决方案:回调函数模式
核心思想
子组件不告诉父组件"我是谁",而是告诉父组件"确认后请通知我"
将"后续要执行的操作"封装为回调函数,保存在子组件内部。父组件只需要在适当时机通知子组件执行即可。
重构后的实现
typescript
// codeLogin.vue - 子组件
type PendingCallback = (() => void) | null;
const pendingCallback = ref<PendingCallback>(null);
const getCode = () => {
if (!isAgree.value) {
// 保存回调:协议确认后执行获取验证码
pendingCallback.value = () => {
executeGetCode();
};
emit('toggleAgreeDialog', true);
return;
}
executeGetCode();
}
const codeLogin = () => {
if (!isAgree.value) {
// 保存回调:协议确认后执行登录
pendingCallback.value = () => {
emit('codeLogin', mobileValue.value, areaCodeValue.value, verifyCodeArg.value);
};
emit('toggleAgreeDialog', true);
return;
}
emit('codeLogin', mobileValue.value, areaCodeValue.value, verifyCodeArg.value);
}
// 供父组件调用
const onAgreementConfirmed = () => {
pendingCallback.value?.();
pendingCallback.value = null;
}
defineExpose({ onAgreementConfirmed });
typescript
// login.vue - 父组件
const handleAgreementConfirm = () => {
toggleIsAgree(true);
toggleAgreeDialog(false);
if (isAccount()) {
doLoginFn(loginTempData);
} else {
// 简单通知子组件执行回调,无需知道具体是什么操作
codeLoginInstance.value?.onAgreementConfirmed();
}
}
数据流对比
重构前:
┌─────────┐ 发送按钮类型 ┌─────────┐ 根据类型判断 ┌─────────┐
│ 子组件 │ ─────────────→ │ 父组件 │ ─────────────→ │ 子组件 │
└─────────┘ └─────────┘ └─────────┘
重构后:
scss
┌─────────┐ 保存回调 ┌─────────┐ 通知执行 ┌─────────┐
│ 子组件 │ ─────────────→ │ 父组件 │ ─────────────→ │ 子组件 │
└─────────┘ 请求显示弹窗 └─────────┘ onConfirmed └─────────┘
(不传类型) (不传参数)
方案对比
| 维度 | 枚举类型传递 | 回调函数模式 |
|---|---|---|
| 耦合度 | 高,父组件依赖子组件枚举 | 低,父组件只调用方法 |
| 扩展性 | 差,新增类型需改两处 | 好,只改子组件 |
| 职责划分 | 模糊,逻辑分散 | 清晰,子组件自治 |
| 代码量 | 需要枚举文件 | 无额外文件 |
| 可测试性 | 差,依赖外部枚举 | 好,逻辑内聚 |
适用场景
回调函数模式适用于以下场景:
-
异步确认流程:如本文的协议确认、二次确认弹窗等
-
多入口单出口:多个触发点,但后续处理由同一个组件负责
-
子组件业务自治:子组件的业务逻辑不应该泄露给父组件
注意事项
-
回调清理:执行完回调后记得置空,避免重复执行
-
错误处理:回调执行可能失败,需要考虑异常情况
-
状态同步 :确保回调执行时,相关状态(如
isAgree)已更新
总结
组件解耦的核心原则是让每个组件只关心自己的职责。当发现父组件需要了解子组件的内部实现细节时,就是重构的信号。
回调函数模式是一种简单有效的解耦手段,它将"做什么"的决策权留给子组件,父组件只负责"何时做"的协调。这种控制反转的思想,在很多设计模式中都有体现,值得在日常开发中灵活运用。