文章目录
写过 Vue 的同学都知道,组件通信是个大话题。props 向下传,emit 向上发,听起来简单,但实际项目中各种复杂场景让人头大。今天深入聊聊组件间通信的进阶技巧------插槽和事件传递。
一、插槽基础
插槽让组件的内容可以由父组件决定:
vue
<!-- components/MyCard.vue -->
<template>
<div class="card">
<slot />
</div>
</template>
<style scoped>
.card {
border: 1px solid #eee;
border-radius: 8px;
padding: 1rem;
}
</style>
使用时:
vue
<MyCard>
<h2>标题</h2>
<p>这是卡片内容</p>
</MyCard>
二、默认内容
插槽可以设置默认内容,父组件不传时显示:
vue
<!-- components/MyButton.vue -->
<template>
<button class="btn">
<slot>
点击我 <!-- 默认内容 -->
</slot>
</button>
</template>
vue
<!-- 使用默认内容 -->
<MyButton />
<!-- 覆盖默认内容 -->
<MyButton>提交</MyButton>
三、具名插槽
一个组件可能有多个区域需要填充:
vue
<!-- components/ArticleCard.vue -->
<template>
<article class="article-card">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer" />
</footer>
</article>
</template>
使用:
vue
<ArticleCard>
<template #header>
<h2>文章标题</h2>
</template>
<p>文章正文内容...</p>
<template #footer>
<span>2024-01-15</span>
</template>
</ArticleCard>
四、作用域插槽
父组件想用子组件的数据怎么办?作用域插槽:
vue
<!-- 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>
父组件自定义渲染:
vue
<UserList>
<template #default="{ user, index }">
<span>{{ index }}. {{ user.name }} ({{ user.age }}岁)</span>
</template>
</UserList>
五、高阶:表格组件
作用域插槽最经典的场景是表格组件:
vue
<!-- 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>
使用:
vue
<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:
vue
<!-- 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>
父组件监听:
vue
<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 的语法糖:
vue
<!-- 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>
使用:
vue
<script setup lang="ts">
const keyword = ref('')
</script>
<template>
<SearchInput v-model="keyword" />
<p>搜索词: {{ keyword }}</p>
</template>
Vue 3.4+ 可以用 defineModel 简化:
vue
<!-- components/SearchInput.vue -->
<script setup lang="ts">
const keyword = defineModel<string>()
</script>
<template>
<input v-model="keyword" placeholder="搜索..." />
</template>
八、多个 v-model
一个组件可以有多个双向绑定:
vue
<!-- 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>
使用:
vue
<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>
九、透传属性
有时候组件只是一个包装器,需要把所有属性传给内部元素:
vue
<!-- components/MyButton.vue -->
<script setup lang="ts">
// 不声明 props,属性会自动透传到根元素
</script>
<template>
<button class="my-button">
<slot />
</button>
</template>
使用时,type、disabled 等属性会自动传给 <button>:
vue
<MyButton type="submit" disabled>提交</MyButton>
渲染结果:
html
<button class="my-button" type="submit" disabled>提交</button>
禁用透传:
vue
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
const attrs = useAttrs()
</script>
<template>
<div>
<input v-bind="attrs" />
</div>
</template>
十、组件引用
有时候需要调用子组件的方法:
vue
<!-- 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>
父组件:
vue
<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 模块,让你的样式管理更规范。
相关文章
延伸阅读
内容有帮助?点赞、收藏、关注三连!评论区等你 💪