文章目录
- 一、插槽基础
- 二、默认内容
- 三、具名插槽
- 四、作用域插槽
- 五、高阶:表格组件
- 六、事件传递:emit
- 七、v-model 双向绑定
- 八、多个 v-model
- 九、透传属性
- 十、组件引用
- 总结
写过 Vue 的同学都知道,组件通信是个大话题。props 向下传,emit 向上发,听起来简单,但实际项目中各种复杂场景让人头大。今天深入聊聊组件间通信的进阶技巧——插槽和事件传递。
一、插槽基础
插槽让组件的内容可以由父组件决定:
<!-- components/MyCard.vue --> <template> <div class="card"> <slot /> </div> </template> <style scoped> .card { border: 1px solid #eee; border-radius: 8px; padding: 1rem; } </style>使用时:
<MyCard> <h2>标题</h2> <p>这是卡片内容</p> </MyCard>二、默认内容
插槽可以设置默认内容,父组件不传时显示:
<!-- components/MyButton.vue --> <template> <button class="btn"> <slot> 点击我 <!-- 默认内容 --> </slot> </button> </template><!-- 使用默认内容 --> <MyButton /> <!-- 覆盖默认内容 --> <MyButton>提交</MyButton>三、具名插槽
一个组件可能有多个区域需要填充:
<!-- components/ArticleCard.vue --> <template> <article class="article-card"> <header> <slot name="header" /> </header> <main> <slot /> <!-- 默认插槽 --> </main> <footer> <slot name="footer" /> </footer> </article> </template>使用:
<ArticleCard> <template #header> <h2>文章标题</h2> </template> <p>文章正文内容...</p> <template #footer> <span>2024-01-15</span> </template> </ArticleCard>四、作用域插槽
父组件想用子组件的数据怎么办?作用域插槽:
<!-- components/UserList.vue --> <script setup lang="ts"> const users = [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 }, { id: 3, name: 'Charlie', age: 35 } ] </script> <template> <ul> <li v-for="user in users" :key="user.id"> <slot :user="user" :index="user.id"> {{ user.name }} <!-- 默认渲染 --> </slot> </li> </ul> </template>父组件自定义渲染:
<UserList> <template #default="{ user, index }"> <span>{{ index }}. {{ user.name }} ({{ user.age }}岁)</span> </template> </UserList>五、高阶:表格组件
作用域插槽最经典的场景是表格组件:
<!-- components/DataTable.vue --> <script setup lang="ts"> interface Column { key: string title: string } const props = defineProps<{ columns: Column[] data: any[] }>() </script> <template> <table> <thead> <tr> <th v-for="col in columns" :key="col.key"> {{ col.title }} </th> </tr> </thead> <tbody> <tr v-for="(row, index) in data" :key="index"> <td v-for="col in columns" :key="col.key"> <slot :name="col.key" :row="row" :value="row[col.key]"> {{ row[col.key] }} </slot> </td> </tr> </tbody> </table> </template>使用:
<script setup lang="ts"> const columns = [ { key: 'name', title: '姓名' }, { key: 'age', title: '年龄' }, { key: 'actions', title: '操作' } ] const users = [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 } ] </script> <template> <DataTable :columns="columns" :data="users"> <!-- 自定义年龄列 --> <template #age="{ value }"> <span :class="{ 'text-red': value > 28 }"> {{ value }}岁 </span> </template> <!-- 自定义操作列 --> <template #actions="{ row }"> <button @click="edit(row)">编辑</button> <button @click="delete(row)">删除</button> </template> </DataTable> </template>六、事件传递:emit
子组件向父组件传数据,用emit:
<!-- components/Counter.vue --> <script setup lang="ts"> const count = ref(0) const emit = defineEmits<{ change: [value: number] reset: [] }>() const increment = () => { count.value++ emit('change', count.value) } const reset = () => { count.value = 0 emit('reset') } </script> <template> <div> <p>计数: {{ count }}</p> <button @click="increment">+1</button> <button @click="reset">重置</button> </div> </template>父组件监听:
<script setup lang="ts"> const handleChange = (value: number) => { console.log('计数变化:', value) } const handleReset = () => { console.log('已重置') } </script> <template> <Counter @change="handleChange" @reset="handleReset" /> </template>七、v-model 双向绑定
v-model本质是:value+@update:value的语法糖:
<!-- components/SearchInput.vue --> <script setup lang="ts"> const props = defineProps<{ modelValue: string }>() const emit = defineEmits<{ 'update:modelValue': [value: string] }>() const input = (e: Event) => { emit('update:modelValue', (e.target as HTMLInputElement).value) } </script> <template> <input :value="modelValue" @input="input" placeholder="搜索..." /> </template>使用:
<script setup lang="ts"> const keyword = ref('') </script> <template> <SearchInput v-model="keyword" /> <p>搜索词: {{ keyword }}</p> </template>Vue 3.4+ 可以用defineModel简化:
<!-- components/SearchInput.vue --> <script setup lang="ts"> const keyword = defineModel<string>() </script> <template> <input v-model="keyword" placeholder="搜索..." /> </template>八、多个 v-model
一个组件可以有多个双向绑定:
<!-- components/DateRange.vue --> <script setup lang="ts"> const startDate = defineModel<Date>('startDate') const endDate = defineModel<Date>('endDate') </script> <template> <div class="date-range"> <input type="date" v-model="startDate" /> <span>至</span> <input type="date" v-model="endDate" /> </div> </template>使用:
<script setup lang="ts"> const start = ref<Date>() const end = ref<Date>() </script> <template> <DateRange v-model:start-date="start" v-model:end-date="end" /> </template>九、透传属性
有时候组件只是一个包装器,需要把所有属性传给内部元素:
<!-- components/MyButton.vue --> <script setup lang="ts"> // 不声明 props,属性会自动透传到根元素 </script> <template> <button class="my-button"> <slot /> </button> </template>使用时,type、disabled等属性会自动传给<button>:
<MyButton type="submit" disabled>提交</MyButton>渲染结果:
<buttonclass="my-button"type="submit"disabled>提交</button>禁用透传:
<script setup lang="ts"> defineOptions({ inheritAttrs: false }) const attrs = useAttrs() </script> <template> <div> <input v-bind="attrs" /> </div> </template>十、组件引用
有时候需要调用子组件的方法:
<!-- components/Modal.vue --> <script setup lang="ts"> const visible = ref(false) const open = () => { visible.value = true } const close = () => { visible.value = false } // 暴露方法给父组件 defineExpose({ open, close }) </script> <template> <Teleport to="body"> <div v-if="visible" class="modal"> <slot /> <button @click="close">关闭</button> </div> </Teleport> </template>父组件:
<script setup lang="ts"> const modalRef = ref<{ open: () => void; close: () => void }>() const showModal = () => { modalRef.value?.open() } </script> <template> <button @click="showModal">打开弹窗</button> <Modal ref="modalRef"> <h2>弹窗内容</h2> </Modal> </template>总结
组件通信技巧汇总:
| 方式 | 方向 | 场景 |
|---|---|---|
| Props | 父→子 | 传递数据 |
| Emit | 子→父 | 传递事件 |
| v-model | 双向 | 表单组件 |
| 插槽 | 父→子 | 内容分发 |
| 作用域插槽 | 子→父 | 子组件数据供父组件渲染 |
| defineExpose | 父→子 | 调用子组件方法 |
下一篇聊聊全局样式与 CSS 模块,让你的样式管理更规范。
相关文章
入门篇三:Nuxt4组件自动导入:写代码少敲一半字
入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子
延伸阅读
nuxt4完整系列,持续更新中。。,欢迎来逛逛
内容有帮助?点赞、收藏、关注三连!评论区等你 💪