Vue3父子组件相互调用方法深度剖析‌

Vue3 父子组件相互调用方法深度剖析


目录

  1. 前言
  2. 父子组件调用基本概念回顾
  3. 父组件调用子组件方法

    • 3.1 传统 Options API 中使用 $ref
    • 3.2 Vue3 Composition API 中使用 ref + expose
    • 3.3 图解:父调子流程示意
  4. 子组件调用父组件方法

    • 4.1 通过 $emit 触发自定义事件(Options / Composition)
    • 4.2 使用 props 传回调函数
    • 4.3 使用 provide / inject 共享方法
    • 4.4 图解:子调父流程示意
  5. 非嵌套组件间的通信方式简述

    • 5.1 全局事件总线(Event Bus)
    • 5.2 状态管理(Pinia/Vuex)
  6. 综合示例:聊天室场景

    • 6.1 目录结构与功能需求
    • 6.2 代码实现与图解
  7. 常见误区与注意事项
  8. 总结

前言

在 Vue3 开发中,父组件与子组件之间的双向调用几乎是最常见的需求之一:父组件需要主动调用子组件的内置方法或更新子组件状态;同时,子组件也常常需要告诉父组件“我这边发生了一件事”,触发父组件做出反应。Vue2 时期我们主要靠 $ref$emitprops 来完成这类交互,而 Vue3 推出了 Composition API、defineExposesetup 中可注入 emit 等机制,使得父子之间的方法调用更灵活、更类型安全。

本文将从底层原理和多种实战场景出发,深度剖析父组件调用子组件方法、子组件调用父组件方法 的所有常见套路,并配上代码示例ASCII 图解,帮助你彻底搞懂 Vue3 中父子组件相互调用的各种姿势。


父子组件调用基本概念回顾

  1. 组件实例与 DOM 节点区分

    • 在 Vue 中,一个组件的“实例”与它对应的 DOM 元素是分离的。要调用组件内部的方法,必须先拿到那个组件实例——在模板里就是用 ref="compRef",然后在父组件脚本里通过 const comp = ref(null) 拿到。
    • comp.value 指向的是组件实例,而非它最外层的 DOM 节点。内部定义在 methods(Options)或 setup 返回的函数才能被调用。
  2. Vue2 vs Vue3 的差异

    • 在 Vue2 中,父组件用 $refs.compRef.someMethod() 调用子组件中定义的 someMethod; 而子组件则 $emit('eventName', payload) 触发自定义事件,父组件在模板上写 <child @eventName="handleEvent"/> 监听。
    • 在 Vue3 Composition API 中,建议在子组件内使用 defineExpose({ someMethod }) 明确暴露给父组件;父组件依旧用 ref 引用组件实例并调用;子组件触发事件时仍可用 emit('eventName', payload) 或直接通过 props 回调方式调用。

父组件调用子组件方法

3.1 传统 Options API 中使用 $ref

<!-- ChildOptions.vue -->
<template>
  <div>
    <p>子组件计数:{{ count }}</p>
    <button @click="increment">子组件 +1</button>
  </div>
</template>

<script>
export default {
  data() {
    return { count: 0 };
  },
  methods: {
    increment() {
      this.count++;
    },
    reset(val) {
      this.count = val;
    }
  }
};
</script>
<!-- ParentOptions.vue -->
<template>
  <div>
    <ChildOptions ref="childComp" />
    <button @click="callChildIncrement">父调用子 increment()</button>
    <button @click="callChildReset">父调用子 reset(100)</button>
  </div>
</template>

<script>
import ChildOptions from './ChildOptions.vue';
export default {
  components: { ChildOptions },
  methods: {
    callChildIncrement() {
      this.$refs.childComp.increment();
    },
    callChildReset() {
      this.$refs.childComp.reset(100);
    }
  }
};
</script>
  • 流程:父模板上用 ref="childComp",父实例的 this.$refs.childComp 就是子组件实例对象,直接调用其内部 methods
  • 局限:如果子组件使用 Composition API 或 script setup,这种方式依然可以,但在 <script setup> 中需配合 defineExpose(见下文)。

3.2 Vue3 Composition API 中使用 ref + expose

在 Vue3 <script setup> 里,子组件的内部函数默认对外是“私有”的。若要父组件调用,需要显式暴露:

<!-- ChildComposition.vue -->
<template>
  <div>
    <p>子组件计数:{{ count }}</p>
    <button @click="increment">子组件 +1</button>
  </div>
</template>

<script setup>
import { ref, defineExpose } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}

function reset(val) {
  count.value = val;
}

// 暴露给父组件:允许父组件通过 ref 调用这两个方法
defineExpose({ increment, reset });
</script>
<!-- ParentComposition.vue -->
<template>
  <div>
    <ChildComposition ref="childComp" />
    <button @click="callChildIncrement">父调用子 increment()</button>
    <button @click="callChildReset">父调用子 reset(200)</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import ChildComposition from './ChildComposition.vue';

const childComp = ref(null);

function callChildIncrement() {
  childComp.value.increment();
}

function callChildReset() {
  childComp.value.reset(200);
}
</script>

说明:

  1. defineExpose({ ... }):将需要被父组件访问的方法暴露给外部,否则父组件拿到的 ref.value 并不会包含这些内部函数;
  2. childComp.value 就是子组件实例,已包含 incrementreset 两个可调用方法;

3.3 图解:父调子流程示意

[ ParentComposition.vue ]
─────────────────────────────────────────────────
1. 模板:<ChildComposition ref="childComp" />
2. 脚本: const childComp = ref(null)
3. 用户点击 “父调用子 increment()” 按钮
    │
    ▼
4. 执行 callChildIncrement() { childComp.value.increment() }
    │
    ▼
[ 子组件实例 ]
─────────────────────────────────────────────────
5. increment() 方法被触发,count++
6. 子组件视图更新,显示新 count

ASCII 图示:

Parent (button click)
   ↓
parentRef.childComp.increment()
   ↓
Child.increment() → count.value++
   ↓
Child 重新渲染

子组件调用父组件方法

4.1 通过 $emit 触发自定义事件

这是最常见的方式,无论 Options API 还是 Composition API,都遵循同样的思路。子组件通过 emit('eventName', payload) 向上触发事件,父组件在模板里以 @eventName="handler" 监听。

Options API 示例

<!-- ChildEmitOptions.vue -->
<template>
  <button @click="notifyParent">子组件通知父组件</button>
</template>

<script>
export default {
  methods: {
    notifyParent() {
      // 子组件向父组件发送 'childClicked' 事件,携带 payload
      this.$emit('childClicked', { msg: '你好,父组件!' });
    }
  }
};
</script>
<!-- ParentEmitOptions.vue -->
<template>
  <div>
    <ChildEmitOptions @childClicked="handleChildClick" />
  </div>
</template>

<script>
import ChildEmitOptions from './ChildEmitOptions.vue';
export default {
  components: { ChildEmitOptions },
  methods: {
    handleChildClick(payload) {
      console.log('父组件收到子组件消息:', payload.msg);
      // 父组件执行其他逻辑…
    }
  }
};
</script>

Composition API 示例

<!-- ChildEmitComposition.vue -->
<template>
  <button @click="notify">子组件发射事件</button>
</template>

<script setup>
import { defineEmits } from 'vue';

const emit = defineEmits(['childClicked']);

function notify() {
  emit('childClicked', { msg: 'Hello from Child!' });
}
</script>
<!-- ParentEmitComposition.vue -->
<template>
  <ChildEmitComposition @childClicked="onChildClicked" />
</template>

<script setup>
import ChildEmitComposition from './ChildEmitComposition.vue';

function onChildClicked(payload) {
  console.log('父组件监听到子事件:', payload.msg);
}
</script>

说明:

  • <script setup> 中,使用 const emit = defineEmits(['childClicked']) 来声明可触发的事件;
  • 父组件在模板上直接用 @childClicked="onChildClicked" 来监听;

4.2 使用 props 传回调函数

有时子组件也可以接收一个函数类型的 prop,父组件将自己的方法以 prop 传入,子组件直接调用。

<!-- ChildPropCallback.vue -->
<template>
  <button @click="handleClick">调用父传入的回调</button>
</template>

<script setup>
import { defineProps } from 'vue';

const props = defineProps({
  onNotify: {
    type: Function,
    required: true
  }
});

function handleClick() {
  props.onNotify('来自子组件的消息');
}
</script>
<!-- ParentPropCallback.vue -->
<template>
  <ChildPropCallback :onNotify="parentMethod" />
</template>

<script setup>
import ChildPropCallback from './ChildPropCallback.vue';

function parentMethod(message) {
  console.log('父组件通过 props 接收:', message);
}
</script>

要点:

  • ChildPropCallback 定义了一个函数类型的 prop,名为 onNotify
  • 父组件以 :onNotify="parentMethod" 传入自身的方法;
  • 子组件在内部直接调用 props.onNotify(...)
  • 这种方式在 TypeScript 环境下更易于类型推导,但语义略显“耦合”。

4.3 使用 provide / inject 共享方法

当父组件与子组件之间有多层嵌套时,逐层传递 props 或事件可能显得繁琐。可利用 Vue3 的依赖注入机制,让父组件将某个方法“提供”出去,任意后代组件都可“注入”并调用。

<!-- ParentProvide.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <Intermediate />
  </div>
</template>

<script setup>
import { provide } from 'vue';
import Intermediate from './Intermediate.vue';

function parentMethod(msg) {
  console.log('父组件收到:', msg);
}

// 父组件提供方法给后代
provide('parentMethod', parentMethod);
</script>
<!-- Intermediate.vue -->
<template>
  <div>
    <h3>中间层组件</h3>
    <ChildInject />
  </div>
</template>

<script setup>
import ChildInject from './ChildInject.vue';
</script>
<!-- ChildInject.vue -->
<template>
  <button @click="callParent">注入调用父方法</button>
</template>

<script setup>
import { inject } from 'vue';

const parentMethod = inject('parentMethod');

function callParent() {
  parentMethod && parentMethod('Hello via provide/inject');
}
</script>

说明:

  • ParentProvide 通过 provide('parentMethod', parentMethod) 给整棵组件树中提供一个名为 parentMethod 的方法;
  • ChildInject 可以在任意深度中通过 inject('parentMethod') 拿到这个函数,直接调用;
  • 这种做法适合多层嵌套,避免 props/emit “层层传递”;但要注意依赖隐式性,代码可读性稍差。

4.4 图解:子调父流程示意

[ ParentProvide.vue ]
────────────────────────────────────────────
1. provide('parentMethod', parentMethod)

[ Intermediate.vue ]
────────────────────────────────────────────
2. 渲染子组件 <ChildInject />

[ ChildInject.vue ]
────────────────────────────────────────────
3. inject('parentMethod') → 拿到 parentMethod 引用
4. 用户点击按钮 → callParent() → parentMethod('msg')
   ↓
5. 执行 ParentProvide 中的 parentMethod,输出“Hello via provide/inject”

ASCII 图示:

ParentProvide
 │ provide('parentMethod')
 ▼
Intermediate
 │
 ▼
ChildInject (inject parentMethod)
 │ click → parentMethod(...)
 ▼
ParentProvide.parentMethod 执行

非嵌套组件间的通信方式简述

本文聚焦 父子组件 间的调用,但若两个组件并非父子关系,也可通过以下方式通信:

5.1 全局事件总线(Event Bus)

// bus.js
import mitt from 'mitt';
export const bus = mitt();
<!-- ComponentA.vue -->
<script setup>
import { bus } from '@/bus.js';

function notify() {
  bus.emit('someEvent', { data: 123 });
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { bus } from '@/bus.js';
import { onMounted, onUnmounted } from 'vue';

function onSomeEvent(payload) {
  console.log('收到事件:', payload);
}

onMounted(() => {
  bus.on('someEvent', onSomeEvent);
});
onUnmounted(() => {
  bus.off('someEvent', onSomeEvent);
});
</script>
:Vue2 时代常用 Vue.prototype.$bus = new Vue() 做事件总线;Vue3 推荐使用轻量的 mitt

5.2 状态管理(Pinia/Vuex)

将共享数据或方法放到全局 Store 中,组件通过挂载到 Store 上的 action 或 mutation 来“调用”它。

// store/useCounter.js (Pinia)
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
    }
  }
});
<!-- ComponentA.vue -->
<script setup>
import { useCounterStore } from '@/store/useCounter';

const counter = useCounterStore();
function doIncrement() {
  counter.increment();
}
</script>
<!-- ComponentB.vue -->
<script setup>
import { useCounterStore } from '@/store/useCounter';
import { computed } from 'vue';

const counter = useCounterStore();
const countValue = computed(() => counter.count);
</script>
<template>
  <div>当前 count:{{ countValue }}</div>
</template>
这种方式适合跨层级、无嵌套限制、业务逻辑更复杂的场景,但会稍微增加项目耦合度。

综合示例:聊天室场景

为了将上述原理集中演示,下面以一个简单的“聊天室”举例。父组件管理登录与连接状态,子组件分别展示消息列表并发送新消息。

6.1 目录结构与功能需求

src/
├─ services/
│   └─ stompService.js  (前文封装的 STOMP 服务)
├─ components/
│   ├─ ChatRoom.vue      (父组件:登录/订阅/断开)
│   ├─ MessageList.vue   (子组件:显示消息列表)
│   └─ MessageInput.vue  (子组件:输入并发送消息)
└─ main.js

功能

  • 登录后建立 STOMP 连接并订阅 /topic/chat
  • MessageList 监听父组件传入的 messages 数组并渲染;
  • MessageInput 输入内容后向父组件发出 send 事件,父组件通过 stompService.send() 通知后端;
  • 后端广播到 /topic/chat,父组件通过 stompService.subscribe() 接收并将消息推入 messages

6.2 代码实现与图解

6.2.1 ChatRoom.vue (合并前面的示例,略作精简)

<template>
  <div class="chat-room">
    <div class="header">
      <span v-if="!loggedIn">未登录</span>
      <span v-else>用户:{{ username }}</span>
      <button v-if="!loggedIn" @click="login">登录</button>
      <button v-else @click="logout">退出</button>
      <span class="status">状态:{{ connectionStatus }}</span>
    </div>
    <MessageList v-if="loggedIn" :messages="messages" />
    <MessageInput v-if="loggedIn" @send="handleSend" />
  </div>
</template>

<script setup>
import { ref, computed, onUnmounted } from 'vue';
import * as stompService from '@/services/stompService';
import MessageList from './MessageList.vue';
import MessageInput from './MessageInput.vue';

const loggedIn = ref(false);
const username = ref('');
const messages = ref([]);

// 监听消息
function onMsg(payload) {
  messages.value.push(payload);
}

const connectionStatus = computed(() => {
  if (!stompService.stompClient) return 'DISCONNECTED';
  return stompService.stompClient.connected ? 'CONNECTED' : 'CONNECTING';
});

function login() {
  const name = prompt('请输入用户名:', '用户_' + Date.now());
  if (!name) return;
  username.value = name;
  stompService.connect(
    () => {
      stompService.subscribe('/topic/chat', onMsg);
      stompService.send('/app/chat', { user: name, content: `${name} 加入群聊` });
      loggedIn.value = true;
    },
    (err) => console.error('连接失败', err)
  );
}

function logout() {
  stompService.send('/app/chat', { user: username.value, content: `${username.value} 离开群聊` });
  stompService.unsubscribe('/topic/chat', onMsg);
  stompService.disconnect();
  loggedIn.value = false;
  username.value = '';
  messages.value = [];
}

function handleSend(text) {
  const msg = { user: username.value, content: text };
  messages.value.push(msg); // 本地回显
  stompService.send('/app/chat', msg);
}

onUnmounted(() => {
  if (loggedIn.value) logout();
});
</script>

<style scoped>
.chat-room { max-width: 600px; margin: 0 auto; padding: 16px; }
.header { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
.status { margin-left: auto; font-weight: bold; }
button { padding: 4px 12px; background: #409eff; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #66b1ff; }
</style>

6.2.2 MessageList.vue

<template>
  <div class="message-list" ref="listRef">
    <div v-for="(m, i) in messages" :key="i" class="message-item">
      <strong>{{ m.user }}:</strong>{{ m.content }}
    </div>
  </div>
</template>

<script setup>
import { watch, nextTick, ref } from 'vue';
const props = defineProps({ messages: Array });
const listRef = ref(null);

watch(
  () => props.messages.length,
  async () => {
    await nextTick();
    const el = listRef.value;
    if (el) el.scrollTop = el.scrollHeight;
  }
);
</script>

<style scoped>
.message-list { height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 8px; background: #fafafa; }
.message-item { margin-bottom: 6px; }
</style>

6.2.3 MessageInput.vue

<template>
  <div class="message-input">
    <input v-model="text" placeholder="输入消息,按回车发送" @keydown.enter.prevent="send" />
    <button @click="send">发送</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const emit = defineEmits(['send']);
const text = ref('');

function send() {
  const t = text.value.trim();
  if (!t) return;
  emit('send', t);
  text.value = '';
}
</script>

<style scoped>
.message-input { display: flex; margin-top: 12px; }
.message-input input { flex: 1; padding: 6px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; }
.message-input button { margin-left: 8px; padding: 6px 12px; background: #67c23a; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #85ce61; }
</style>

常见误区与注意事项

  1. 未使用 defineExpose 导致父拿不到子方法

    • Vue3 <script setup> 下,若没在子组件调用 defineExpose,父组件通过 ref 拿到的实例并不包含内部方法,调用会报错。
  2. 子组件 $emit 事件拼写错误或未声明

    • <script setup> 必须使用 const emit = defineEmits(['eventName']) 声明可触发的事件;若直接调用 emit('unknownEvent') 而父未监听,则无效果。
  3. 过度使用 provide / inject 导致依赖隐式

    • 虽然能跨层级传递方法,但可读性和维护性下降,建议只在层级较深且数据流复杂时使用。
  4. 循环引用导致内存泄漏

    • 若在父组件 onUnmounted 未正确 unsubscribedisconnect,可能导致后台持久订阅,内存不释放。
  5. 使用 $refs 太早(组件尚未挂载)

    • mounted 之前调用 ref.value.someMethod() 会拿到 null,建议在 onMounted 或点击事件回调中再调用。

总结

本文系统深入地剖析了 Vue3 父子组件相互调用方法 的多种方案与实践,包括:

  • 父组件调用子组件

    • Vue2 的 $refs
    • Vue3 Composition API 中的 ref + defineExpose
  • 子组件调用父组件

    • $emit 触发自定义事件;
    • props 传回调函数;
    • provide/inject 跨层级注入;
  • 还简要介绍了 非嵌套 组件间的全局事件总线和状态管理两种辅助方式;
  • 最后通过一个“实时聊天室”示例,结合 StompJS + WebSocket,演示了实际应用中如何在父子组件间高效调用方法、交互数据。

希望通过丰富的代码示例ASCII 流程图解注意事项,能帮助你快速掌握 Vue3 中父子组件调用方法的全方位技巧。在开发过程中,灵活选择 $emitpropsexposeprovide/inject 等机制,将让你的组件通信更清晰、可维护性更高。

VUE
最后修改于:2025年05月31日 12:35

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日