大家好,我是 前端架构师 - 大卫。
更多优质内容请关注微信公众号 @程序员大卫。
初心为助前端人🚀,进阶路上共星辰✨,
您的点赞👍与关注❤️,是我笔耕不辍的灯💡。
背景
假设有 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>
✅ 优点:写法直观,类型推导良好。
❌ 缺点:每层组件都要处理
ref
和defineExpose
,不够灵活。
方法二:使用 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 类型友好 | 每层都要处理 ref 和 expose |
层级不深或可控时 |
方法二 Proxy |
极简,封装后复用性高 | 组件链复杂,想省事时 | |
方法三 register |
解耦组件间依赖 | 类型书写繁琐,耦合事件逻辑 | 对 ref 管理和注册有需求时 |
根据项目复杂度和团队偏好选择合适方式,简单场景优先用 defineExpose
,复杂嵌套推荐 Proxy
方案。