Vue 3 调用深层组件方法,这 3 招够用了!

大家好,我是 前端架构师 - 大卫

更多优质内容请关注微信公众号 @程序员大卫

初心为助前端人🚀,进阶路上共星辰✨,

您的点赞👍与关注❤️,是我笔耕不辍的灯💡。

背景

假设有 A、B、C 三个组件,A 组件想要调用最内层 C 组件的方法。由于组件之间是逐层嵌套的,A 并不能直接访问 C,这就需要我们通过一些手段将 C 的方法"暴露"到 A。

下面介绍三种常见的实现方式。

方法一:传统方式 ------ defineExpose 层层传递

通过 defineExpose 将方法逐层向上传递,让 A 组件最终可以访问到 C 的方法。

C 组件

html 复制代码
<script setup lang="ts">
import { ref } from "vue";

const num = ref(0);

defineExpose({
  increment: () => num.value++,
});
</script>

<template>
  <h1>C: {{ num }}</h1>
</template>

B 组件

html 复制代码
<script setup lang="ts">
import { shallowRef } from "vue";
import C from "./C.vue";

const cRef = shallowRef<InstanceType<typeof C>>();

defineExpose({
  increment: () => cRef.value?.increment(),
});
</script>

<template>
  <h1>B</h1>
  <C ref="cRef" />
</template>

A 组件

html 复制代码
<script setup lang="ts">
import { shallowRef } from "vue";
import B from "./B.vue";

const bRef = shallowRef<InstanceType<typeof B>>();
</script>

<template>
  <h1 @click="bRef?.increment">A</h1>
  <B ref="bRef" />
</template>

✅ 优点:写法直观,类型推导良好。

❌ 缺点:每层组件都要处理 refdefineExpose,不够灵活。

方法二:使用 Proxy 自动透传

通过封装 useRefExpose 工具方法,将内部组件实例通过 Proxy 代理出去,无需每层手动暴露方法。

A 组件和 C 组件不变。

B 组件

html 复制代码
<script setup lang="ts">
import { ref } from "vue";
import C from "./C.vue";
import { useRefExpose } from "@/hooks/useRefExpose";

const childRef = ref<InstanceType<typeof C>>();

defineExpose(useRefExpose(childRef));
</script>

<template>
  <h1>B</h1>
  <C ref="childRef" />
</template>

⚠️注意:如果 B 组件还想把自己的方法暴露出去,那么可以这么写:

ts 复制代码
defineExpose({
  ...useRefExpose(childRef),
  otherMethod: () => {
    console.log("xxxxx");
  },
});

useRefExpose 实现

ts 复制代码
import { Ref, ComponentPublicInstance } from "vue";

export function useRefExpose<T extends ComponentPublicInstance>(
  ref: Ref<T | undefined | null>
) {
  return new Proxy({} as T, {
    get(_, prop) {
      return ref.value?.[prop as keyof T];
    },
    has(_, prop) {
      return ref.value ? prop in ref.value : false;
    },
  });
}

✅ 优点:无需一层层暴露,只需要封装一次代理逻辑。

方法三:使用注册回调的方式传递实例

通过事件注册的方式,将 C 组件实例通过 B 传递给 A。

C 组件不变。

B 组件

html 复制代码
<script setup lang="ts">
import { ComponentPublicInstance } from "vue";
import C from "./C.vue";

type CInstance = InstanceType<typeof C>;

const emit = defineEmits<{
  (e: "register", el: CInstance): void;
}>();

const setRef = (c: Element | ComponentPublicInstance | null) => {
  emit("register", c as CInstance);
};
</script>

<template>
  <h1>B</h1>
  <C :ref="setRef" />
</template>

可以看到 B 组件的 ts 类型不够友好,setRef 函数使用了 c as CInstance,所以 B 组件可以改成:

html 复制代码
<script setup lang="ts">
import { shallowRef, watchEffect } from "vue";
import C from "./C.vue";

type CInstance = InstanceType<typeof C>;

const emit = defineEmits<{
  (e: "register", el: CInstance): void;
}>();

const cRef = shallowRef<CInstance | null>(null);

watchEffect(() => {
  if (cRef.value) {
    emit("register", cRef.value);
  }
});
</script>

<template>
  <h1>B</h1>
  <C ref="cRef" />
</template>

A 组件

A 组件还需要引入 C 的类型,所以 register 的这种方式,感觉不够好。

html 复制代码
<script setup lang="ts">
import { shallowRef } from "vue";
import B from "./B.vue";
import type C from "./C.vue";

type CInstance = InstanceType<typeof C>;

const cInstance = shallowRef<CInstance | null>(null);

const register = (el: CInstance) => {
  cInstance.value = el;
};
</script>

<template>
  <h1 @click="cInstance?.increment">A</h1>
  <B @register="register" />
</template>

✅ 优点:组件解耦,不需要使用 defineExpose

❌ 缺点:A 需要依赖 C 的类型,B 组件的 ref 类型也需手动指定,TS 写法略显繁琐。

总结

方法 优点 缺点 适用场景
方法一 defineExpose 简单直观,TS 类型友好 每层都要处理 refexpose 层级不深或可控时
方法二 Proxy 极简,封装后复用性高 组件链复杂,想省事时
方法三 register 解耦组件间依赖 类型书写繁琐,耦合事件逻辑 ref 管理和注册有需求时

根据项目复杂度和团队偏好选择合适方式,简单场景优先用 defineExpose,复杂嵌套推荐 Proxy 方案。

相关推荐
岁岁种桃花儿15 分钟前
Vue核心语法第十一篇:绑定样式
前端·javascript·vue.js
程序员黑豆8 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC8 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea9 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望9 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少9 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波00710 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis10 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝11 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps