在 Vue 项目里管理弹窗组件:用 ref 还是用 props?
在做业务后台时,页面上常常会有很多弹窗:新增、编辑、详情、排班表......这些弹窗如果直接写在页面里,很快就会把父组件挤爆,于是我们会想到"抽成子组件"。但这时往往会遇到一个问题:父组件到底该用 ref 调子组件方法,还是用 props + 事件 来控制子组件?这篇文章就围绕这个问题,把我们刚才讨论过的内容系统梳理一下。
两种常见的控制方式
1. 用 props + 事件 控制子组件(推荐)
思路:
- 父组件通过 props 把"状态"传给子组件(例如:是否可见、当前模式、需要编辑的行数据等)。
- 子组件通过 事件 告诉父组件"我保存好了""我关闭了",父组件再决定要不要刷新列表等。
父组件负责的状态(示例):
- 列表 & 查询相关
- 当前页、每页条数、筛选条件等。
- 和弹窗有关的最小状态
- 某个编辑弹窗当前是否显示(例如:编辑弹窗的 visible 布尔值)。
- 编辑模式(新增 / 编辑)。
- 当前被编辑的这一行数据(或它的 id)。
子组件负责的状态:
- 表单字段对象。
- 校验规则。
- 弹窗里的 loading 状态。
- 内部下拉列表、内部接口请求等。
优点:
- 单向数据流清晰:父组件的数据流向子组件,子组件通过事件"回报"结果。
- 职责边界清楚:父管"业务流程",子管"弹窗内部细节"。
- 高度可复用:这个弹窗子组件以后可以在很多父组件里复用,只要把必要的数据通过 props 给它。
- 迁移到 Vue 3 更顺畅:这种模式正是 Vue 3 推荐的,迁移时只需要把语法细节稍微调整一下(比如变成 v-model:visible、用 defineEmits 等)。
可以把这种模式理解为:> 父组件不"命令"子组件做事,而是"给它数据 + 监听它发出的事件"。
2. 用 $refs.xxx.xxx() 直接调用子组件方法
思路:
- 父组件给子组件一个 ref 名,比如 groupEdit。
- 父组件在需要时,通过 this.$refs.groupEdit.open('edit', row) 之类的方式,直接调用子组件的方法,来打开弹窗、注入数据。
父组件负责的内容:
- 仍然要记住子组件对外暴露的 API,例如:open(mode, row)。
- 需要维护正确的 ref 名称,并保证子组件提供了对应方法。
子组件负责的内容:
- 定义若干供父组件调用的方法,比如 open、close、reset 等。
- 在这些方法里再去设置自身的 visible、表单数据等内部状态。
优点:
- 用起来直观:一眼就知道"调用这个函数就会打开弹窗",读起来像是"调用组件的 API",容易理解。
- 当这个弹窗 只在一个地方用 时,这种方式在短期内也能"够用"。
缺点:
- 耦合度高:
- 父组件必须知道子组件的 ref 名字。
- 父组件必须知道子组件有哪些公共方法,以及这些方法的参数签名。
- 不便于复用:
- 在别的页面复用这个弹窗时,也不得不按同样的办法注册 ref,调用同名方法。
- 不够 Vue 化:
- Vue 更鼓励"数据 + 事件"的声明式方式,而不是处处用 ref 去命令式调用。
Vue 3 迁移视角下的比较
如果你将来准备把项目慢慢升级到 Vue 3,或者用 Composition API /
props + 事件 在 Vue 3 的表现
- Vue 3 官方依然推荐:
- 父传子:通过 props / v-model。
- 子传父:通过 emit 事件。
- 迁移时,只需要:
- 把 .sync 写法变成 v-model:xxx。
- 在子组件里用 defineProps、defineEmits 等新语法替代老的 props / this.$emit 写法。
核心思想不变,只是"换一层语法皮"。这意味着:今天用 props + 事件 管子组件,将来改 Vue 3 时,不用推翻设计,只是重写语法。
$refs 调子组件方法在 Vue 3 的表现
- 传统 Options API 下 $refs 依然可以用。
- 但如果你改用
- 这样一来:
- 子组件要加"暴露 API"的声明;
- 父组件仍要通过 ref 调用这些方法;
- 一旦你多次重构组件结构,容易出"小坑"。
也就是说:这种以 $refs 为中心的方式,在 Vue 3 里不能说用不了,但和新的组合式风格有点"别扭",迁移时需要额外适配。
当页面上有"很多弹窗"时该怎么设计
现实业务中,一个页面上可能有:
- 新增班组弹窗
- 编辑班组弹窗
- 排班表弹窗
- 详情弹窗
- 审核意见弹窗
- ...
如果所有弹窗的表单、校验、loading、内部状态,统统塞到父组件的 data 里,父组件会非常臃肿,而且很多状态和"列表主流程"并没有直接关系。一个比较健康的拆分策略是:
父组件只维护"与业务流程相关的最小状态"
例如:
- 当前激活的是哪个弹窗:
- 某个 bool:例如编辑弹窗的 visible。
- 弹窗模式:新增 / 编辑。
- 当前作用的业务对象:
- 比如当前选中的一行数据,或当前的业务 id。
所有这些数据都直接体现"页面业务流程":比如"我要新增一个班组","我要编辑这行班组","我要查看这个班组的排班表"。
子组件维护"弹窗内部的一切细节"
例如:
- 表单字段。
- 校验规则。
- 内部 loading。
- 内部下拉选项、接口请求逻辑。
- 内部的 UI 结构。
这样,即使页面上有很多弹窗:
- 父组件只是多了一些简单的"状态标志 + 当前对象"。
- 复杂的逻辑都被封装在一个个相对独立的子组件里。
无论你最终选择 props + 事件 还是 $refs,这个拆分边界是更重要的设计点。
什么时候更适合用 props + 事件
可以优先选用这一套的场景:
- 你希望:
- 组件可复用。
- 以后升级 Vue 3、改 Composition API 时成本低。
- 团队代码风格更偏声明式、数据驱动。
- 一个弹窗可能会在多个页面被复用,例如:
- 公共的"选择用户"弹窗。
- 多处使用的"编辑班组"弹窗等。
- 你想要更清晰的边界:父组件描述"业务流程",子组件只管"具体表现和交互"。
总结成一句话:> 只要不是非常临时的一次性组件,props + 事件 一般都是更稳妥的首选方案。
什么时候 $refs 也可以接受
尽管不推荐作为默认选择,但在一些情况下,$refs 也是可以使用的:
- 这个弹窗 只会在当前页面里使用,没有复用需求。
- 你很希望有一个"像函数一样"的入口,比如:
- 在某个复杂流程中,需要多次、不同参数地调用子组件的能力。
- 调用链比较长时,用 $refs.xxx.open(param) 比一层层传 props 来得更好读。
- 团队内部对 $refs 的用法有统一约定:
- 比如统一所有侧滑弹窗子组件都对外提供 open(row) 方法。
- 这样即使用 $refs,也还算规范。
哪怕如此,也建议:
- 不要在一个组件里到处用 $refs 驱动逻辑。
- 尽量将 $refs 使用场景局限在"少数真正需要命令式行为"的地方。
总结:实际项目里的推荐组合
综合上面所有点,可以给出一个比较落地的建议组合:
- 通用弹窗 / 表单组件:
- 用 props + 事件 管理;
- 父组件保持最小状态,子组件封装内部逻辑;
- 为未来迁移 Vue 3 做好准备。
- 特殊场景、强命令式的东西(少数):
- 可以用 $refs + 子组件暴露少量方法;
- 但要克制使用,并在团队内达成共识。
如果你现在正在重构一个老页面、抽离一堆弹窗组件,一个实用的操作顺序是:
- 先尽量用 props + 事件 把复杂表单弹窗都抽出去。
- 只有在确实"很不方便"时,再为某个子组件补一个 open() 形式的 API,配合 $refs 使用。
这样既能兼顾当前开发效率,也不会把未来的维护成本提前埋雷。