组件通信完全指南:父传子与子传父(React + Vue + TypeScript)
·
你不是在传递数据,你是在搭建组件之间的“对话桥梁”
在一个现代前端应用中,组件就像乐高积木——每个组件独立、可复用,但当它们组合在一起时,必须能够相互“交谈”。最常见也最基础的两个通信方向就是:
-
父传子:父组件把数据交给子组件去展示或使用
-
子传父:子组件把用户的操作结果或内部状态告诉父组件
无论你用的是 React、Vue 还是 Angular,这两个模式都万变不离其宗。更妙的是,结合 TypeScript,我们可以让这些数据传递变得类型安全,告别运行时才发现 props 写错或事件参数格式不对的尴尬。
本文将通过清晰的对比、完整的代码示例以及 TypeScript 的类型加持,让你彻底掌握组件通信的“标准姿势”。
1. 父传子:数据自上而下
- React + TypeScript 实现
- 在 React 中,父组件通过 JSX 属性传递数据,子组件通过 props 参数接收。使用 TypeScript 时,我们定义一个 interface 来描述 props 的形状。
// Child.tsx
interface ChildProps {
title: string; // 必传
count: number;
isVisible?: boolean; // 可选
}
const Child: React.FC<ChildProps> = ({ title, count, isVisible = true }) => {
return (
<div>
<h3>{title}</h3>
<p>当前计数:{count}</p>
{isVisible && <span>我是可见的</span>}
</div>
);
};
// Parent.tsx
const Parent = () => {
return (
<>
<Child title="来自父组件的标题" count={42} />
<Child title="另一个实例" count={100} isVisible={false} />
</>
);
};
- 类型的好处:如果你在父组件中漏传了 title,TypeScript 会直接报错;如果传了错误类型(比如 count 传字符串),也会立刻被捕获。
- Vue 3 + TypeScript 实现
- Vue 3 的组合式 API 配合
<!-- Child.vue -->
<script setup lang="ts">
defineProps<{
title: string
count: number
isVisible?: boolean
}>()
</script>
<template>
<div>
<h3>{{ title }}</h3>
<p>当前计数:{{ count }}</p>
<span v-if="isVisible">我是可见的</span>
</div>
</template>
<!-- Parent.vue -->
<template>
<Child title="Vue 传递的数据" :count="42" />
<Child title="另一个" :count="100" :is-visible="false" />
</template>
<script setup lang="ts">
import Child from './Child.vue'
</script>
注意:Vue 模板中 isVisible 需要写成 is-visible(props 的 kebab-case 写法)。
2. 子传父:数据自下而上
子传父通常用于子组件想通知父组件“我这儿发生了某件事”,比如按钮被点击、输入框内容变化、表单项提交等。核心模式是:父组件将一个函数(或监听一个事件)传给子组件,子组件在合适的时机调用该函数并传递数据。
- React + TypeScript 实现(回调函数)
- React 中最直接的方式是父组件把一个回调函数作为 prop 传给子组件,子组件调用这个函数并把数据作为参数传回去。
// Child.tsx
interface ChildProps {
onSendMessage: (message: string) => void; // 回调函数的类型签名
}
const Child: React.FC<ChildProps> = ({ onSendMessage }) => {
const handleClick = () => {
onSendMessage("Hello from Child!");
};
return <button onClick={handleClick}>发送给父组件</button>;
};
// Parent.tsx
const Parent = () => {
const handleChildMessage = (msg: string) => {
console.log("父组件收到:", msg);
// 可以在这里更新父组件的状态、发起网络请求等
};
return <Child onSendMessage={handleChildMessage} />;
};
- 类型保护:onSendMessage 的类型明确要求参数是 string,如果子组件调用时传了别的类型(比如数字),TypeScript 会报错,有效防止了运行时 bug。
- Vue 3 + TypeScript 实现(自定义事件)
- Vue 使用 emit 触发自定义事件,父组件用 @事件名 监听。
<!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits<{
(e: 'sendMessage', message: string): void // 声明事件及参数类型
}>()
const send = () => {
emit('sendMessage', '来自子组件的消息')
}
</script>
<template>
<button @click="send">发送给父组件</button>
</template>
- 父组件监听:
<!-- Parent.vue -->
<template>
<Child @send-message="handleMessage" />
</template>
<script setup lang="ts">
const handleMessage = (msg: string) => {
console.log('父组件收到:', msg)
}
</script>
Vue 中事件名推荐使用 kebab-case(send-message),但在 defineEmits 的类型声明里可以用 camelCase(sendMessage),两者都会自动转换。
3. 组合实战:计数器双向通信
- 父组件展示一个总数,子组件提供“+1”和“-1”按钮,点击后子组件把变化后的新值通知父组件,父组件更新自己的状态。
- React 版本
// CounterChild.tsx
interface CounterChildProps {
onValueChange: (newValue: number) => void;
initialValue?: number;
}
const CounterChild: React.FC<CounterChildProps> = ({ onValueChange, initialValue = 0 }) => {
const [value, setValue] = React.useState(initialValue);
const increment = () => {
const newVal = value + 1;
setValue(newVal);
onValueChange(newVal);
};
const decrement = () => {
const newVal = value - 1;
setValue(newVal);
onValueChange(newVal);
};
return (
<div>
<button onClick={decrement}>-</button>
<span>{value}</span>
<button onClick={increment}>+</button>
</div>
);
};
// CounterParent.tsx
const CounterParent = () => {
const [total, setTotal] = React.useState(0);
return (
<div>
<h2>父组件显示的总值:{total}</h2>
<CounterChild onValueChange={(newVal) => setTotal(newVal)} />
</div>
);
};
- Vue 版本
<!-- CounterChild.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
initialValue?: number
}>()
const emit = defineEmits<{
(e: 'update:value', newValue: number): void
}>()
const value = ref(props.initialValue ?? 0)
const increment = () => {
value.value++
emit('update:value', value.value)
}
const decrement = () => {
value.value--
emit('update:value', value.value)
}
</script>
<template>
<div>
<button @click="decrement">-</button>
<span>{{ value }}</span>
<button @click="increment">+</button>
</div>
</template>
父组件:
<template>
<div>
<h2>父组件显示的总值:{{ total }}</h2>
<CounterChild @update:value="total = $event" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import CounterChild from './CounterChild.vue'
const total = ref(0)
</script>
4. 常见错误与最佳实践
| 错误现象 | 原因分析 | 解决方法 |
|---|---|---|
| 子组件 props 接收不到数据 | 父组件属性名写错,或者子组件未声明该 prop | 检查属性名大小写和命名一致性 |
| 子组件修改了 props 的值并报错 | 在 React/Vue 中 props 是只读的,不能直接修改 | 将 props 的值复制到本地 state 或 ref 再修改 |
| 子传父时回调被多次调用 | 子组件内部可能重复触发了事件 | 检查事件绑定是否重复,使用 useCallback 等 |
| TypeScript 报错:属性不存在 | 未正确声明 props 的类型接口 | 定义明确的 interface 并导出 |
最佳实践
- 单向数据流:始终保证数据从父组件流向子组件(props 只读),子组件通过回调或事件通知父组件修改数据,而不是直接修改 props。
- 用 TypeScript 锁定类型:为每一个 prop 和回调函数编写明确的类型,这会在编码阶段就发现大多数通信错误。
- 避免过深的 prop drilling:如果三层以上的组件需要传递同一个数据,考虑使用 Context(React)或 Provide/Inject(Vue)代替逐层传递。
- 给可选 props 提供默认值:React 中可以用 defaultProps 或解构默认值;Vue 中可以用 withDefaults 宏。
- 命名规范:React 回调 props 通常以 on 开头(如 onClick、onChange);Vue 自定义事件名使用 kebab-case。
5. 总结
| 通信分向 | 核心机制 | react实现 | vue3实现 |
|---|---|---|---|
| 父传子 | Props | Child name={value} / | Child :name=“value” / |
| 子传父 | 回调函数 / 自定义事件 | onXxx={handler} + 调用 | emit(‘xxx’, data) + 监听 |
- TypeScript 的关键作用
- 父传子:用 interface ChildProps 确保父组件传递的数据类型正确,子组件内部使用 props 时也有智能提示。
- 子传父:用函数类型 (data: Type) => void 或 defineEmits<{…}> 约束回调参数的类型,防止传错数据结构。
掌握了父传子和子传父,你就掌握了组件通信的“第一原理”。在此之上,无论你将来学习全局状态管理(Redux、Pinia)还是更高级的模式,都会觉得顺理成章。
现在,打开你的编辑器,试着用 TypeScript 写一个简单的待办事项组件:父组件展示列表,子组件负责新增待办(子传父)和展示每一项(父传子)。相信你会立刻感受到类型安全带来的踏实感。
Happy Coding! 🧩
更多推荐
所有评论(0)