首页 / Nuxt 4 入门教程 / 组件、props、插槽

Nuxt 4 入门教程

组件、props、插槽

本教程共 50 篇 · 第 12 篇 · 更新于 2026-08-08 · 约 7 分钟阅读

NuxtNuxt4Vue3组件propsemits插槽

本节目标:掌握 Vue 组件之间怎么通信——用 props 从父传数据给子、用 emits 从子发事件给父,再用插槽让父组件往子组件里塞内容。

把界面拆成一个个组件后,它们必须能「对话」才有用。Vue 的通信规则很清晰:数据向下用 props,事件向上用 emits,内容嵌套用插槽。这章把这三件套讲透。

12-1

Vue 的组件形成一棵树。父组件里用子组件,就像搭积木。通信方向约定俗成:

  • 父 → 子:传数据,用 props
  • 子 → 父:通知事情发生(常附带数据),用 emits
  • 父 → 子(内容):决定子组件内部某块显示什么,用 插槽 slot

记住「props 向下、事件向上」这八个字,后面就不会乱。

12-2

defineProps() 是编译期宏(不是普通函数),用来声明「这个组件能接收哪些外部传入的属性」。

推荐用基于类型的写法(配合 TypeScript 最直观):

<!-- Child.vue -->
<script setup lang="ts">
const props = defineProps<{
  title: string
  count?: number
}>()
</script>

<template>
  <h2>{{ title }}</h2>
  <p>数量:{{ count }}</p>
</template>

父组件这样传值:

<!-- Parent.vue -->
<template>
  <Child title="欢迎" :count="3" />
</template>

要点:

  • 普通文本直接写 title="欢迎";其他类型(数字、对象、表达式)要加 :(即 v-bind 简写),如 :count="3"
  • <script> 里通过 props.title 访问;在 <template> 里直接写 title 即可,编译器知道它是 props。
  • ? 表示可选,count? 不传也不会报错。
Warning

把 props 当成只读。子组件里不要去修改 props 的值——这会破坏「父组件拥有数据」的约定,导致父子紧耦合、难以维护。需要基于 props 做本地状态,用 props 的值去初始化一个本地变量。

12-3

props 负责「给数据」,emits 负责「说事情发生了」。子组件用 defineEmits() 声明自己会发出哪些事件,需要时调用 emit

<!-- Child.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  (e: 'submit', value: string): void
}>()

function onClick() {
  emit('submit', '来自子组件的数据')
}
</script>

<template>
  <button @click="onClick">提交</button>
</template>

父组件监听这个事件:

<!-- Parent.vue -->
<script setup lang="ts">
function handleSubmit(payload: string) {
  console.log('收到子组件消息:', payload)
}
</script>

<template>
  <Child @submit="handleSubmit" />
</template>

这样,子组件点按钮 → 发出 submit 事件并带上数据 → 父组件的 handleSubmit 被调用。数据沿着「事件向上」的路线回到了父级。

Tip

模板里也能直接用 $emit<button @click="$emit('submit', val)">。但代码稍多时,还是在 <script> 里用 const emit = defineEmits(...) 更清楚。

12-4

常见场景:一个输入框组件,父级传初始值、用户改了再通知父级。这种「又传又收」用 v-model 最简洁。Vue 3.4+ 提供 defineModel() 宏:

<!-- Child.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>

<template>
  <input v-model="model" />
</template>

父组件直接:

<!-- Parent.vue -->
<script setup lang="ts">
const text = ref('')
</script>

<template>
  <Child v-model="text" />
</template>

Vue 自动保证:用户在子组件输入,text 同步更新,无需手写 emit + props 来回接。这是处理表单类组件的首选。

12-5

有时你想让子组件留出一块「空位」,由父组件填内容。这就是插槽(slot)。最基础的默认插槽:

<!-- Card.vue -->
<template>
  <div class="card">
    <slot>这里放默认内容(父没传时显示)</slot>
  </div>
</template>

父组件往里塞:

<template>
  <Card>
    <p>这段内容会填进 Card 的插槽里</p>
  </Card>
</template>

需要多个插槽时,用具名插槽

<!-- Layout.vue -->
<template>
  <div>
    <header><slot name="header" /></header>
    <main><slot /></main>
    <footer><slot name="footer" /></footer>
  </div>
</template>

父组件指定名字:

<template>
  <Layout>
    <template #header>顶部标题</template>
    <template #default>正文内容</template>
    <template #footer>页脚信息</template>
  </Layout>
</template>
Note

插槽传的是「内容/模板」,props 传的是「数据」。简单区分:要让子组件显示父给的用 props;要让子组件显示父给的一段界面用插槽。

12-6

当组件嵌套很深,中间层只是为了把 props 往下传,会出现「透传地狱」(prop drilling)。Vue 提供 provide / inject 跨层直接通信:

// 祖先组件
provide('theme', 'dark')

// 任意后代组件
const theme = inject('theme')

不过日常小项目用 props + emits 就够了;跨层共享、全局状态更推荐用 Nuxt 的 useState 或 Pinia(后续章节讲)。provide/inject 知道有这个选项即可。

12-7

把前面三件套串起来,看一个真实场景:父组件显示文章列表,子组件是一个「点赞按钮」,点一下把次数回传给父。

<!-- LikeButton.vue(子) -->
<script setup lang="ts">
const props = defineProps<{ initial: number }>()
const emit = defineEmits<{ (e: 'liked', count: number): void }>()

// 本地状态基于 props 初始化,不直接改 props
const count = ref(props.initial)

function onClick() {
  count.value++
  emit('liked', count.value) // 把最新值抛给父
}
</script>

<template>
  <button @click="onClick">赞 {{ count }}</button>
</template>
<!-- Parent.vue(父) -->
<script setup lang="ts">
const total = ref(0)
function onLiked(n: number) {
  total.value = n // 接收子组件抛来的值
}
</script>

<template>
  <LikeButton :initial="0" @liked="onLiked" />
  <p>总点赞:{{ total }}</p>
</template>

逐行看流向:父用 :initial 把初始值向下传进子 → 子用 props.initial 接住并初始化本地 count → 用户点按钮,子改本地状态并 emit('liked', ...) 向上通知父 → 父的 @liked 处理函数拿到值更新 total。一条数据,先下后上,各管各的,互不越界。

Warning

注意子组件里我们没有直接改 props.initial,而是用 ref(props.initial) 建了个本地副本。props 是父的,子只该读;要改就改自己的本地副本,这是 Vue 通信里最容易写错的一点——直接给 props 赋值在开发模式下会报警告。

12-8

回头看三件套,落地时照这个思路挑:

  • 父要把「一个值」给子 → defineProps
  • 子要把「一件事发生了」告诉父 → defineEmits
  • 父要往子里塞「一整段界面」→ 插槽 slot
  • 表单类既要传初始值又要收改动 → defineModelv-model)。
  • 跨很多层传同一份数据 → 先用 props/emits 试试,真乱了再上 provide/injectuseState、Pinia。
Tip

一个实用心法:数据永远「谁创建、谁拥有」。父创建的数据用 props 借给子用,子想改就通过事件把意图交还给父去改。守住这条,组件之间就不会缠成一团。

12-9

组件通信三件套:用 defineProps 让父把数据传给子(只读、别改);用 defineEmits 让子把事件发给父;用 slot 让父往子里塞内容。v-model 则是表单类双向绑定的省事写法。掌握这几点,你就能把 Nuxt 页面拆成干净、可复用的组件了。

第一部分到此结束。你已经从「Nuxt 是什么」一路走到「组件怎么通信」,打好了 Nuxt 开发的地基。后续章节会在此基础上展开路由、数据获取、服务端能力等更进阶的主题。

12-9 组件设计的基本原则

好的组件设计是前端项目可维护的基石。在 Nuxt 里,由于组件自动导入,你不需要担心 import 语句的繁琐,可以把更多精力放在组件本身的质量上。

一个组件应该只做一件事。如果你发现一个 .vue 文件超过两百行,或者同时处理数据获取、表单验证、UI 渲染,那它可能承担了太多职责。把它拆成更小的组件,每个组件各司其职,通过 props 和 emits 协作。

另外,组件的 props 接口就是它的”合同”。定义清楚每个 prop 的类型、是否必填、默认值,调用方就能正确使用,不需要翻看组件源码。在 TypeScript 支持下,这种合同会被编译器强制执行,漏传或传错类型会立刻报错。

12-10 调试组件通信的实用技巧

组件通信出问题时,最快的排查方式是借助 Vue Devtools 浏览器插件。它能让你实时查看组件树、每个组件的 props 值、发出的事件,以及响应式数据的变化。当 props 传值不符合预期时,在 Devtools 里一眼就能看出来。

另一个有用的调试手段是在子组件的 watch 里打印 props 的变化。当父组件更新 props 时,子组件的 watcher 会触发,打印出旧值和新值。这能帮你确认数据传递的时机和内容是否正确。

对于复杂的组件通信场景(如多层嵌套的 provide/inject),建议在关键节点加日志,追踪数据从源头到目的地的完整路径。这样当数据不符合预期时,你能快速定位是哪一层出了问题。