{T}

VTable 实例方法透传与选择列控制

概述

沿官方案例扩展表格时,前一节解决的是 row-click、current-change、selection-change 这类"事件怎么向外透传";这一节开始处理另一类需求:页面层想主动控制表格,例如切换某行选中、清空选中。这类需求靠事件不够,必须让外层拿到表格实例能力。本章重点落在选择列(type="selection")如何立刻暴露实例透传问题,以及用 defineExpose 配合 exposeMethodsUtils 把内部 el-table 方法优雅地暴露出去。事件是"表格告诉外层发生了什么",实例方法是"外层主动指挥表格做什么",两者互补。

学习目标

  • 区分事件透传(发生了什么)与实例方法透传(我要它做什么)两条能力线
  • 认识单选多选案例同时暴露"选择状态"与"实例控制"两类交互链路
  • 理解选择列 type="selection" 本身简单,却立刻带来实例透传问题
  • 明白页面层 ref 指向的是 VTable 包装组件,而不是内部 el-table 实例
  • 用 defineExpose 把内部表格实例方法暴露给外层
  • 方法很多时用 exposeMethodsUtils 批量生成 expose 方法
  • 理解 defineExpose 应暴露"页面真正想调用的方法"而非整个内部实例
  • 认识事件出口与方法出口是成熟基础组件并行具备的两条线

一、事件透传之外,外层还要能调用表格实例方法

前一节主要解决的是 row-click、current-change、selection-change 这类"事件怎么向外透传"。这一节处理的另一类常见需求是:页面层想主动控制表格,例如切换某行选中、清空选中。这类需求靠事件不够,必须让外层拿到表格实例能力。

text
事件透传  -> 表格告诉外层发生了什么
实例方法透传 -> 外层主动指挥表格做什么

表格组件一旦进入多选、单选、程序化控制阶段,就一定会碰到实例透传问题。事件和实例方法是两条不同的扩展路线,不能混为一谈。这一步标志着 VTable 开始从"被动展示组件"变成"可被页面主动操控的组件"。

二、单选多选案例暴露两条交互链路

实现官方的单选高亮与多选列时,先接了 highlight-current-row 和 type="selection"。但这一步真正值得关注的,不只是页面上出现高亮和复选框列,更重要的是它带出了两类操作方式:用户点击后表格自己变化,以及页面层主动调用表格实例方法让它变化。这正是选择类表格最常见的真实需求。

text
用户点击      -> selection-change
页面主动控制  -> toggleRowSelection / clearSelection

如果只支持点击,不支持实例控制,很多实际业务会很快卡住。这一节是 VTable 从"被动接事件"走向"主动受控制"的关键节点。

三、选择列简单,但立刻暴露实例透传问题

做多选列时,最直接的改动非常简单:在列配置最前面加一列,type="selection",再给个宽度。但就是这个简单的列,会立刻把一个更深的问题带出来:页面层想通过按钮去切换选中某行,结果 ref 指向的只是 VTable 组件实例,并不是内部真实的 el-table 实例。

ts
const columns: VTableColumnType[] = [
  { type: "selection", width: 55 },
  { prop: "date", label: "Date" },
  { prop: "name", label: "Name" }
]

选择列本身只是入口,真正难点是"如何从页面层操纵它"。一旦出现"按钮控制选中/清空",就要进入实例代理问题。

四、ref 指向 VTable 拿到的是包装实例,不是 el-table

在页面层直接写 multipleTableRef.value.toggleRowSelection(...) 却发现方法不存在,根因非常典型:你页面里 ref 到的是 VTable,而不是 el-table。除非 VTable 主动把内部实例方法暴露出来,否则外层根本拿不到。

text
page ref -> VTable instance(不是 el-table instance)

这是 Vue 组件封装天然带来的隔离层,不是 bug。只要你封了业务组件,外层就不再自动拥有内部原生实例方法。这一步非常适合理解"组件封装边界到底意味着什么"。

五、defineExpose 暴露内部表格实例(第一步)

最直接的修复:在 VTable 内部定义 tableRef 让它绑定到内部 el-table,再用 defineExpose 暴露出去。这样页面层拿到 VTable 实例时,就能进一步访问 tableRef 或者直接访问包装后的方法。

ts
const tableRef = ref<TableInstance>()

defineExpose({
  tableRef
})
Vue SFC
<el-table ref="tableRef" ... />

这是最基础的透传方法,但还不够优雅。页面层如果还要写 multipleTableRef.value.tableRef.value.clearSelection(),说明 API 仍然偏丑,所以 defineExpose 往往还只是第一步。

六、方法多时抽 exposeMethodsUtils 实例代理工具

和前一节的事件透传思路非常类似,既然表格实例方法很多,那就不要一个个手写 clearSelection、toggleRowSelection、toggleAllSelection。更好的做法是先收一份方法名数组,再写一个工具函数批量生成 expose 出去的方法对象。这和 forwardEventsUtils 是同一种工程思路:名称列表、工具函数、批量生成。

ts
export function exposeMethodsUtils(
  instanceRef: Ref<any>,
  methodNames: readonly string[]
) {
  const exposeMethods: Record<string, (...args: any[]) => void> = {}

  methodNames.forEach((methodName) => {
    exposeMethods[methodName] = (...args: any[]) => {
      const target = instanceRef.value?.[methodName]
      if (typeof target === "function") {
        target(...args)
      }
    }
  })

  return exposeMethods
}

事件多时抽事件工具,实例方法多时抽实例代理工具,这种思路是通用的,价值在于减少重复样板代码。

七、defineExpose 应暴露方法,而非整个内部实例

最终做法更进一步,不只是暴露 tableRef,而是直接暴露 toggleRowSelection、clearSelection。这样页面层调用时变成 multipleTableRef.value.toggleRowSelection(...),而不是 multipleTableRef.value.tableRef.value.toggleRowSelection(...)。

ts
defineExpose({
  ...exposeMethodsUtils(tableRef, ["clearSelection", "toggleRowSelection", "toggleAllSelection"])
})

这种差异非常重要,因为组件 API 的美观程度,很多时候决定了它后续会不会被愿意复用。暴露"最少但够用的方法集"通常比暴露整个实例更合理;一旦页面层依赖了太多内部实例细节,封装边界就会被打穿。好的 defineExpose 应该像设计公共 API,而不是像调试透视窗。

八、事件透传与实例方法透传是两条平行能力线

连续两节其实已经把 VTable 的交互边界补得很清楚:事件透传是表格告诉页面层刚刚发生了什么,实例方法透传是页面层告诉表格现在要做什么,两者并不互相替代,而是互补关系。一个成熟基础组件通常同时拥有事件出口和方法出口;只透传事件不够,很多业务还需要程序化控制,只暴露方法也不够,页面层仍然需要知道用户做了什么。


常见问题

问题原因解决方案
拿到 ref 却调不到 clearSelectionref 指向的是 VTable,不是内部 el-table在 VTable 里通过 defineExpose 主动暴露方法
多选列能点但按钮无法切换选中只做了事件透传,没有做实例方法透传给表格实例方法做一层 expose 代理
页面层要写 tableRef.value.tableRef.value.xxx,非常丑把内部实例结构原样暴露给外层暴露最终方法,不暴露内部实现细节
想支持更多实例方法,代码越来越重复一个个手写 expose 方法用方法名数组 + exposeMethodsUtils 批量生成
以为事件透传就足够误把通知页面层当成允许页面层控制组件明确区分事件出口和方法出口
选择列配置好后还报类型问题列类型对 type、prop 的兼容不够保持本地列类型足够宽松并兼容选择列场景

延伸阅读