理解所谓的 组件
这个组件,我们可能会第一时间的想到是前端中的那些,小的那种交互式的组件,是那种 Web Components
但是,Vue 中它不止这样,我们可以把 Vue 中的组件理解为 Java 中的类
- 在Java中,你不会把所有代码都写在main方法里。你会创建User类、Order类、Product类。每个类都有自己的属性(字段)、行为(方法)和生命周期。
- 而在前端,写一个巨大的HTML文件是扫码的,要人命的。所以 Vue 建议你把页面拆分成一个个组件(比如NavBar、UserProfile、Footer)。
- 一个Vue组件(通常是一个.vue文件)把HTML结构(模板)、JavaScript逻辑(数据与行为)、CSS样式(外观)封装在一起。就像Java类把字段和方法封装在一起一样。
所以,Vue 中,组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。组件化开发是 Vue 的核心思想,它允许你将大型应用拆分成小的、独立的、可复用的部分,就像搭积木一样构建应用。
Vue 组件是可复用的 Vue 实例,拥有自己的模板、数据、方法和生命周期钩子。
在实际应用中,组件常常被组织成一个层层嵌套的树状结构:
组件机制的设计,可以让开发者把一个复杂的应用分割成一个个功能独立组件,降低开发的难度的同时,也提供了极好的复用性和可维护性。
- 模块化: 是从代码逻辑的角度进行划分的;方便代码分层开发,保证每个功能模块的职能单一;
- 组件化: 是从UI界面的角度进行划分的;前端的组件化,方便UI组件的重用。
但是其实,我们认为 Vue 和 Web Components 是互补的技术。
但是默认情况下,Vue 会将任何非原生的 HTML 标签优先当作 Vue
组件处理,而将“渲染一个自定义元素”作为后备选项。这会在开发时导致 Vue
抛出一个“解析组件失败”的警告。要让 Vue
知晓特定元素应该被视为自定义元素并跳过组件解析,我们可以指定 compilerOptions.isCustomElement
这个选项。
如果在开发 Vue 应用时进行了构建配置,则应该在构建配置中传递该选项,因为它是一个编译时选项。
组件相关知识
attributes透传
Attributes 是属性的意思,是传递给组件的 HTML 特性(如
class、style、id
等),它们不属于组件声明的
props,但会被自动继承到组件的根元素上。
特性继承,当你向组件传递未声明为 props 的特性时,Vue
会自动将它们应用到组件的根元素上:
1 | <!-- 父组件使用 --> |
如果不希望根元素继承特性,可以使用
inheritAttrs: false:
1 | <script setup> |
使用 useAttrs() 可以访问所有传递的特性:
1 | <script setup> |
Attributes 透传(Attribute Inheritance)指的是,当父组件传递给子组件的属性不是子组件显式声明的 props 时,这些属性会自动应用到子组件的根元素上。
- 常见的透传属性包括
class、style、id、disabled以及原生事件监听器(如@click)。 - 父组件向子组件传递未声明的属性。
- 如果子组件只有一个根元素,这些属性会自动添加到该根元素上。
- 如果子组件有多个根元素或使用了
<script setup>,需要显式使用v-bind="$attrs"来指定透传目标。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,父组件的模板如下
1 | <!--父组件--> |
单一根元素的子组件就是这样
1 | <!--子组件 ChildComponent --> |
那么,html 的渲染结果就会是这样
1 | <div |
当存在多个根元素时,需要特殊处理
1 | <template> |
可以使用 defineOptions 配置项禁用透传
1 | <script setup> |
props
什么是 Props
Props 是组件之间数据传递的接口,让父组件可以向子组件传递数据。
在实际应用中,父子组件之间的通信是非常常见的场景,可以用于共享状态、配置组件行为、实现交互逻辑等场景。
为了实现数据和内容的传递,Vue 提供了两种重要的机制:Props 和 Slots
Props
是properties的缩写,我们可以简单的把它们理解为,子组件声明的一组「属性」。
Props 是 Vue 组件间通信的一种主要方式,用于父组件向子组件传递数据。它是父子组件之间的桥梁,遵循单向数据流原则(父组件数据更新会流向子组件,但子组件不能直接修改父组件传递过来的 props)。
也就是说,Props 是只读的,子组件不应该修改接收到的 props。它具有单向性
而且父组件在通过 Props
传递数据之前,需要在子组件中明确声明这些属性
定义 Props
定义 Props,使用 TypeScript 的风格,继续我们之前的例子,我们在 WordList 中定义了这样的 Props 类型(接口声明)
1 | // 定义 Props 接口 - 明确规定组件接收的属性结构 |
interface Props:用 TS 接口严格定义了 props 的结构和类型?表示可选属性:父组件可以不传这个属性withDefaults:专门用于给 TS 风格的 props 设置默认值的工具函数
然后,通过 defineProps
接收并设置默认值,这行代码就是实际接收 props 的核心
defineProps<Props>():告诉 Vue “我要接收符合 Props 接口定义的属性”withDefaults(...):给可选的filterBy设置默认值(如果父组件没传,就用'all')- 最终返回的
props对象,就是父组件传递过来的属性集合
Props 的传递
那么 Props 如何传递,父组件 → 子组件
以我们的单词本为例子,在 WordList 组件中,向
WordCard 传递了 props:
1 | <WordCard |
- 使用
:属性名="值"(v-bind 指令)的形式传递动态数据 - 如果传递的是静态值(比如字符串),可以直接写
card-style="compact"
然后WordCard
子组件,在其组件的内部中使用父组件WordList中传递过来的
props
其中,可以看到,WordCard 接收
word、showExample 和 cardStyle
props,使用了:进行绑定,用于渲染单个单词卡片,而 Word 的
Props 定义如下
1 | // script 中通过 props 对象访问 |
没有参数的 v-bind 也可以将一个对象的所有属性都当作 props 传入
使用v-model绑定Props
如何使用v-model 在自定义组件中进行双向数据绑定呢?
- 父组件通过
v-model绑定数据 - 子组件接收
modelValueprops,并通过update:modelValue事件通知父组件更新
使用一个简单的例子帮助理解
1 | <!-- 子组件:CustomInput.vue --> |
1 | <!-- 父组件使用 --> |
如果需要多个双向绑定,可以给 v-model 指定参数:
1 | <!-- 父组件 --> |
更改Props
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
那么,props 的单向传递的,如果我们真的要去修改 props 的属性值的时候,应该如何处理
一般情况下,可以将 props 复制为本地数据,但这仅限制于不需要同步回父组件的情况
通过事件通知父组件更新是最常用的情况,而 v-model 也会导致父组件的 Props 被更改,这种情况就不说了
1 | <!-- 子组件 Child.vue --> |
1 | <!-- 父组件 Parent.vue --> |
总结
所以,使用 Props 是这样的
模板
<template>中可以直接用 props 名称(如word、showExample),但是在script中需要通过props.xxx访问Props 的大小写命名规范
- 在声明 props
时,使用驼峰命名(camelCase):
cardStyle - 在模板传递时,使用连字符命名(kebab-case):
card-style
1
2
3
4
5
6
7<!-- 父组件中 -->
<WordCard :card-style="compact" />
<!-- 子组件中 -->
<script setup>
const props = defineProps(['cardStyle']) // 驼峰接收
</script>- 在声明 props
时,使用驼峰命名(camelCase):
Props 可以和 v-model 结合实现双向绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<!-- 子组件:WordInput.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleInput = (e) => {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="handleInput" />
</template>
<!-- 父组件使用 -->
<WordInput v-model="searchWord" />
data
上面说的 Props 是负责管理组件间传递数据的关键内容,而 data 负责管理组件内部状态
data 是 Vue
组件内部的状态,它存储了与组件相关的数据和信息。data
返回的是一个对象,这个对象中定义的属性会被 Vue
的响应式系统管理,意味着当 data 中的值发生变化时,Vue
会自动更新相关的视图。
- 私有性:仅在当前组件内部生效,不直接对外暴露(区别于
props接收的外部数据)。 - 响应式:
data中的数据被 Vue 监听,当数据变化时,依赖该数据的视图会自动更新(这是 Vue 响应式系统的核心)。 - 函数形式:在组件中,
data必须以函数形式返回一个对象(避免多个组件实例共享同一份数据)。
在 Vue2
的时候,使用选项式API的时候,它的特征尤为明显,在选项式API中data
是一个函数,它返回一个对象,这个对象包含了组件的所有响应式数据。
而且这个对象的组织形式必须是 key:value形式。
1 | <template> |
在这个示例中,data 返回一个对象,其中的
counter 被 Vue 的响应式系统管理。每当 counter
的值改变时,页面会自动更新。
但是在使用 组件式API 时,data 的概念稍有不同。你需要通过
ref 或 reactive 来声明响应式的数据,使用 Vue 3
的 <script setup> 语法,这本质上是 data
的替代方案
例如,若要在组件中添加内部状态,在 <script setup>
中会这样写
1 | // 在 WordCard.vue 中添加内部状态(示例) |
在模板中使用
1 | <template> |
点击按钮切换状态时,视图会自动更新
而且,在 Vue3 中,data()
必须是一个函数,而不是直接返回一个对象。这是因为 Vue
组件可能会被多次复用,如果 data()
是一个对象,那么所有实例将共享同一个数据对象,导致数据冲突。
通过将 data() 定义为函数,每次创建组件实例时,Vue
都会调用这个函数,返回一个新的数据对象,从而确保每个实例的数据是独立的
1 | export default { |
每次创建组件实例时,data()
函数都会被调用,返回一个全新的 message 数据。
在 data() 函数中,不要直接修改 Vue 实例的属性(如
this.$data),而是通过返回对象的方式来定义数据。
而且data 中的状态是组件私有的,外部组件(如父组件
WordList)不能直接修改,只能通过子组件暴露的方法或事件间接影响。
WordCard 有一个 “是否已掌握”
的内部状态,点击按钮切换状态时,视图会自动更新
1 | // WordCard.vue 中添加响应式状态 |
1 | <template> |
那么,WordCard 的 isMastered 状态只能在
WordCard 内部通过 toggleMastered
方法修改,WordList
无法直接操作,若需同步状态,需通过事件通知:
1 | // WordCard.vue 中通过事件将状态变化通知父组件 |
ref和reactive函数
在 Vue 3 中,ref 和 reactive
是实现响应式数据的核心 API,它们用于将普通数据转换为
“响应式数据”(即数据变化时,依赖该数据的视图会自动更新)
ref 是最常用的响应式
API,适用于基本类型(字符串、数字、布尔等)和对象
/ 数组。
- 包裹基本类型:会将基本类型包装为一个
“响应式对象”(本质是一个包含
value属性的对象)。 - 自动解包:在模板中使用时,无需通过
.value访问;但在脚本中必须通过.value访问 / 修改。 - 兼容对象 / 数组:若传入对象或数组,
ref会自动调用reactive处理(内部转换为reactive对象)。
WordCard 中有这么一个 “是否已掌握” 的交互状态:
1 | // WordCard.vue 中使用 ref |
模板中使用(自动解包,无需 .value):
1 | <template> |
而reactive
专门用于将对象或数组转换为响应式数据,不适用于基本类型(如
reactive(123) 无效)。
- 仅适用于对象 / 数组:传入基本类型会被忽略(非响应式)。
- 直接访问属性:无需
.value,可直接修改对象的属性或数组的元素。 - 深层响应式:对象的嵌套属性也会被转为响应式(如
obj.nested.prop变化会触发更新)。
WordList 需要存储 “筛选条件”
的复杂状态(包含搜索关键词和难度):
1 | // WordList.vue 中使用 reactive |
模板中使用:
1 | <template> |
在上述的模板中使用了双向绑定绑定了使用 reactive 包装的
filterState,它把 filterState
变成了一个响应式的数据,在下面中,通过filterState.searchKeyword访问其searchKeyword搜索关键词的属性,并且是双向更新,而且还搞了一个事件按钮去操作设置筛选的难度,使用
set 函数
表单数据使用 reactive 管理是非常合适的选择,例如,在
AddWordForm.vue 中
1 | // 复杂表单对象适合用 reactive |
这里的表单数据是一个包含多个字段的复杂对象,使用
reactive 可以:
- 直接通过属性名访问(如
formData.word),无需.value前缀 - 保持各字段间的关联性,统一管理整个表单状态
- 配合
v-model实现简洁的双向绑定(如<input v-model="formData.word">)
而上面说了ref
可以包裹数组和对象类型,只不过需要在脚本内访问的时候加.value,当
ref 包裹对象类型时,其内部会自动转换为
reactive 对象
1 | // 等价于 const user = reactive({ name: '张三' }) |
这意味着 ref 可以看作是更通用的响应式包装器,而
reactive 是专门针对对象的优化实现。
reactive 对象在以下情况会丢失响应式,需特别注意:
- 直接赋值整个对象(如
formData = { ... }) - 解构赋值(如
const { word } = formData,解构出的word是非响应式的)
怎么办呢?
对需要解构的属性使用
toRefs转换:1
2
3
4import { reactive, toRefs } from 'vue'
const formData = reactive({ word: '', translation: '' })
const { word, translation } = toRefs(formData) // 保持响应式对需要整体替换的对象使用
ref包裹:1
2const formData = ref({ word: '', translation: '' })
formData.value = { word: 'test', translation: '测试' } // 保持响应式
Vue 3 的响应式系统通过 “Proxy 代理” 实现,本质上是追踪它的依赖:
ref对基本类型包装为{ value: ... },通过代理value的getter/setter追踪依赖。reactive通过 Proxy 直接代理对象,追踪对象属性的访问和修改。
当响应式数据被修改时,Vue 会自动触发依赖该数据的视图更新(如
WordList 中 filteredWords 依赖
props.filterBy,若用响应式数据替换
props.filterBy,筛选结果会自动更新)。
| 特性 | ref |
reactive |
|---|---|---|
| 适用类型 | 基本类型(string/number/boolean 等)、对象 / 数组 | 仅对象 / 数组 |
| 访问方式(脚本中) | 需通过 .value 访问 / 修改 |
直接访问 / 修改属性 |
| 访问方式(模板中) | 自动解包,无需 .value |
直接访问属性 |
| 赋值行为 | 可直接替换整个值(ref.value = 新值) |
不能直接替换整个对象(会丢失响应式) |
事件
上面说了,子组件不能直接修改 props 和 data ,如果需要修改,应该通过事件通知父组件
1 | // ❌ 错误:直接修改 props |
Vue 的事件系统是组件间通信的核心机制之一,主要用于子组件向父组件传递数据或触发行为。
所以,什么是事件
Vue
事件是组件间通信的一种方式,允许子组件主动向父组件发送消息(如用户操作、数据变化等),并可携带数据。类似于浏览器的原生事件(如
click),但 Vue 提供了更灵活的自定义事件机制。
定义一个事件
子组件需要先声明自己可以触发哪些事件,类似 “接口约定”,便于类型检查和维护。
1 | // 定义事件类型接口(TypeScript) |
interface Emits:用 TypeScript 定义事件名称和参数类型,确保类型安全defineEmits:Vue 提供的宏函数,用于注册组件可触发的事件,返回一个emit函数
然后定义事件对应触发后的逻辑即可
在<script setup> 中,组件可以显式地通过
defineEmits()宏来声明它要触发的事件
1 | <script setup> |
因为在 <template> 中使用的 $emit
方法不能在组件的 <script setup> 部分中使用,但
defineEmits() 会返回一个相同作用的函数供我们使用
而且defineEmits()
宏不能在子函数中使用。如上所示,它必须直接放置在
<script setup> 的顶级作用域下。
事件触发
子组件通过 emit
函数触发已定义的事件,并传递数据给父组件。
1 | // 触发删除事件(传递id) |
- 调用
emit时,第一个参数必须是defineEmits中声明的事件名 - 后续参数为传递给父组件的数据(可多个,类型需与定义一致)
而且在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件
1 | <!-- MyComponent --> |
其中的@就是事件监听的指令缩写
事件监听
我们可以使用 v-on 指令来监听原生 DOM 事件。v-on 指令也可以缩写为
@
绑定事件时,可以直接将事件名称传入 v-on 指令,并在后面跟随事件处理函数。例如,监听 click 事件:
1 | <template> |
@click="handleClick"表示绑定 click 事件,事件触发时会调用 handleClick 函数。
放到Vue的父子组件中,我们这样监听,父组件通过 @事件名
语法监听子组件触发的事件,并定义处理函数。
1 | <template> |
有时候我们需要传递参数给事件处理函数。可以直接在事件处理函数调用时传入参数。例如
1 | <template> |
@click="handleClick('hello')"表示点击按钮时调用 handleClick,并将 ‘hello’ 作为参数传入
子组件事件传递给父组件
上面说了,子组件无法直接修改父组件的数据(尤其是Props),但可以通过触发事件的方式向父组件传递信息(如用户操作、数据变化等),父组件通过监听事件接收信息并处理
子组件定义可触发的事件
使用
defineEmits声明组件能触发的事件(支持 TypeScript 类型校验),明确事件名称和传递的参数类型。子组件触发事件
通过
emit方法触发已定义的事件,并传递需要给父组件的数据(参数)。父组件监听事件
在使用子组件时,通过
@事件名监听子组件触发的事件,并定义处理函数接收参数。
例如,WordList.vue 作为子组件的时候,这样传递事件
1 | <!-- 子组件 WordList.vue --> |
父组件如何监听(父组件为 App.vue):
1 | <!-- 父组件 App.vue --> |
这样,子组件仅传递事件和数据,由父组件决定如何处理(如修改数据),符合 Vue 单向数据流原则。
父组件传递事件给子组件
Vue 中没有 “父组件直接触发子组件事件” 的原生语法,但可以通过向子组件传递函数类型的 props,让子组件在特定时机调用该函数,间接实现 “父组件向子组件传递事件逻辑”。
父组件定义处理函数
父组件中定义需要子组件执行的逻辑(函数)。
父组件通过 props 传递函数给子组件
将函数作为 props 传递给子组件。
子组件接收函数 props 并调用
子组件在需要的时机(如点击、数据变化)调用父组件传递的函数。
在父组件中,这样定义子组件需要执行的逻辑
1 | <!-- 父组件 App.vue --> |
然后子组件这样接受事件
1 | <!-- 子组件 WordList.vue --> |
这其实本质上是函数的传递,父组件传递的是函数引用,子组件通过调用函数触发父组件逻辑。
访问事件对象
事件对象是浏览器原生事件机制的一部分,包含了事件相关的详细信息,当我们在 Vue 中绑定事件时,可以通过特定方式访问这个事件对象
Vue 中访问事件对象的方式主要有两种,取决于事件处理函数是否需要传递额外参数。
无额外参数时:自动接收事件对象
当事件处理函数不需要传递自定义参数时,Vue 会自动将原生事件对象(
$event)作为第一个参数传入处理函数。1
2
3
4
5
6
7
8
9
10
11
12<template>
<!-- 点击按钮,处理函数自动接收原生事件对象 -->
<button @click="handleClick">点击我</button>
</template>
<script setup>
function handleClick(event) {
// event 即为原生 Event 对象
console.log('触发事件的元素:', event.target) // 输出 <button> 元素
console.log('点击位置 X 坐标:', event.clientX)
}
</script>有额外参数时:使用
$event手动传递当需要向事件处理函数传递自定义参数,同时又需要访问事件对象时,需通过 Vue 提供的特殊变量
$event手动将事件对象传入。1
2
3
4
5
6
7
8
9
10
11
12
13<template>
<!-- 传递 word.id 作为参数,同时通过 $event 传递事件对象 -->
<button @click.stop="handleDelete($event, word.id)">删除</button>
</template>
<script setup>
function handleDelete(event, id) {
// event 是原生事件对象,id 是自定义参数
console.log('删除的单词 ID:', id)
console.log('触发事件的按钮:', event.target) // 输出当前按钮元素
event.stopPropagation() // 等价于 @click.stop 修饰符的效果
}
</script>- 这里的
$event是 Vue 内置的变量,专门用于指代原生事件对象 - 顺序可以灵活调整(如
handleDelete(id, $event)),但需与函数参数对应
- 这里的
原生 Event 对象包含大量属性和方法,常用的有:
| 属性 / 方法 | 说明 |
|---|---|
target |
触发事件的原始元素(最内层元素) |
currentTarget |
绑定事件的元素(当前元素) |
type |
事件类型(如 click、keyup) |
clientX/clientY |
鼠标点击位置相对于浏览器窗口的坐标 |
stopPropagation() |
阻止事件冒泡(等价于 .stop 修饰符) |
preventDefault() |
阻止默认行为(等价于 .prevent 修饰符) |
key |
键盘事件中按下的键名(如 Enter) |
事件修饰符
Vue 提供了事件修饰符简化事件处理逻辑,例如
1 | <!-- 按钮点击时阻止事件冒泡 --> |
.stop:阻止事件冒泡(避免触发父元素的同名事件)。例如,WordCard本身有@click事件,按钮的点击如果不阻止冒泡,会同时触发WordCard的handleClick点击事件。
而事件修饰符(如
.stop、.prevent)本质上是对事件对象方法的简化调用:
@click.stop等价于在函数中调用event.stopPropagation()@click.prevent等价于event.preventDefault(),两者可以同时使用,例如:1
2<!-- 既阻止冒泡,又在函数中访问事件对象 -->
<button @click.stop="handleClick($event, word)">点击</button>真有人这么写吗???????
Vue 常用的事件修饰符如下
.stop - 阻止事件冒泡
- 作用:阻止事件向上冒泡到父元素,避免触发父元素的同名事件。
- 场景:子元素和父元素都绑定了同一事件(如
click)时,防止点击子元素时同时触发父元素的事件。
.prevent - 阻止默认行为
作用:阻止事件的默认行为(如表单提交刷新页面、链接跳转等)。
场景:需要自定义处理逻辑,而非浏览器默认行为时使用。
1
2
3
4
5
6
7<!-- 阻止表单默认提交(避免页面刷新) -->
<form @submit.prevent="handleSubmit">
<button type="submit">提交</button>
</form>
<!-- 阻止链接默认跳转 -->
<a href="https://example.com" @click.prevent="handleLinkClick">点击不跳转</a>
.capture - 事件捕获模式
作用:改变事件触发顺序,让事件在捕获阶段(从父到子)触发,而非默认的冒泡阶段(从子到父)。
场景:需要优先处理父元素事件时使用(较少见)。
1
2
3
4<div @click.capture="handleParent">
<!-- 点击按钮时,先触发父元素的 handleParent,再触发子元素的 handleChild -->
<button @click="handleChild">点击</button>
</div>
.self - 仅自身触发
作用:事件仅在触发事件的元素是自身时才执行,忽略冒泡或捕获阶段传递的事件。
场景:避免父元素响应子元素冒泡上来的事件(与
.stop区别:.self不阻止冒泡,只是不响应冒泡事件)。1
2
3
4<div @click.self="handleParent">
<!-- 点击按钮时,子元素的事件触发后会冒泡到父元素,但父元素的 @click.self 不会执行 -->
<button @click="handleChild">点击</button>
</div>
.once - 仅触发一次
作用:事件处理函数仅执行一次,之后自动解绑。
场景:需要限制事件只能触发一次的场景(如 “同意协议” 按钮)。
1
2<!-- 按钮点击一次后,handleClick 不再执行 -->
<button @click.once="handleClick">仅点击一次有效</button>
.passive - 提升滚动性能
作用:告知浏览器事件处理函数不会调用
preventDefault(),用于优化触摸 / 滚动事件的性能(避免浏览器等待事件处理完成再滚动,减少卡顿)。场景:常用于
touchmove、scroll等频繁触发的事件。1
2<!-- 滚动事件使用 .passive 提升性能 -->
<div @scroll.passive="handleScroll">长列表内容</div>
还有一些常用的就是键鼠的触发事件修饰符了
插槽Slots
插槽(Slots)是 Vue 中实现组件内容分发的强大机制,它允许父组件向子组件中指定位置插入自定义内容,极大地提高了组件的灵活性和复用性。
本质上是子组件中预留的 “占位符”,父组件可以通过插槽向子组件注入内容。这种机制解决了父子组件间内容传递的问题,让组件结构更灵活。
简单理解就是组件内部留一个或多个的插槽位置,可供组件传对应的模板代码进去。插槽的出现,让组件变的更加灵活。
默认插槽
默认插槽是没有名字的插槽,使用 <slot>
标签定义,父组件中直接写在子组件标签之间的内容会被插入到默认插槽中。
子组件中定义
1 | <template> |
这里的 <slot> 标签就是一个占位符,它告诉
Vue:”
这里可以插入父组件的内容,如果父组件没给内容,就显示我里面的
<p> 标签 “。
有时为一个插槽设置具体的后备 (也就是默认的)
内容是很有用的,它只会在没有提供内容的时候被渲染。其中
<p class="footer-text">这是默认的底部内容</p>就是插槽的后备内容
父组件中如何使用这个插槽呢(父组件往这个插槽内插东西)
如果父组件不提供内容,子组件中 <slot>
标签内的默认内容会生效。
1 | <template> |
此时子组件会显示默认内容:
<p class="footer-text">这是默认的底部内容</p>
如果父组件提供内容(替换默认内容)
1 | <template> |
此时子组件的 <slot> 标签会被父组件写的
<div class="custom-footer"> 替换,最终渲染:
<div class="custom-footer">...</div>
具名插槽
当子组件需要多个插槽时,可以给插槽命名,即 “命名插槽”。使用
name 属性指定插槽名称,父组件通过
#插槽名(v-slot 语法糖)来匹配对应插槽。
1 | <header class="container-header"> |
在父组件中使用
其中,使用
v-slot指令把你需要插入的内容插入到插槽中,其缩写为#,和其他指令一样,该缩写只在其有参数的时候才可用。
具名插槽的内容必须使用模板< template ></ template >包裹
1 | <WordContainer> |
那么,这些内容就被插入到这个插槽中了
其实,匿名插槽也具有隐藏的名字default
我的示例中,定义了三个命名插槽:header、search
和 content,分别对应组件的不同区域。
动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名
1 | <base-layout> |
父组件
1 | // 现在 <template>元素中的所有内容都将会被传入相应的插槽。 |
子组件
1 | <template> |
作用域插槽
子传父
作用域插槽允许子组件向插槽传递数据,父组件在填充插槽时可以访问这些数据。这解决了父组件需要使用子组件内部数据的问题。
1 | <main class="container-content"> |
这里通过 :filteredWords="filteredWords"
语法绑定了filteredWords的内容为filteredWords,向插槽传递了数据,父组件可以接收这些数据。
1 | <WordContainer> |
作用域插槽的关键是子组件通过 v-bind
向插槽传递数据,父组件通过解构赋值接收数据。
父传子
在插槽机制中,父组件向子组件传递数据主要通过两种方式:
直接通过 Props 传递(子组件声明 props,父组件绑定传递)
这是最基础的父传子方式,子组件通过
defineProps声明需要接收的数据,父组件在使用子组件时通过属性绑定传递。子组件(
WordContainer.vue):声明 props1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<script setup>
import { defineProps } from 'vue'
// 子组件声明需要接收的 props
const props = defineProps({
title: {
type: String,
default: '单词列表'
},
maxCount: {
type: Number,
default: 100
}
})
</script>
<template>
<div class="word-container">
<!-- 使用父组件传递的 props 数据 -->
<header>
<h2>{{ title }}</h2>
<p>最多显示 {{ maxCount }} 个单词</p>
</header>
<!-- 插槽内容 -->
<slot />
</div>
</template>父组件:传递 props 数据
1
2
3
4
5
6
7
8
9
10
11
12<template>
<div>
<!-- 父组件通过属性绑定传递数据给子组件 -->
<WordContainer
title="我的英语单词本"
:maxCount="50"
>
<!-- 插槽内容 -->
<p>这里是插槽内容</p>
</WordContainer>
</div>
</template>当数据需要被子组件内部逻辑使用(如子组件的计算属性、方法)时,必须通过 props 传递;若只是在插槽内容中显示,则直接使用父组件数据即可。
在插槽内容中直接使用父组件数据(插槽内容本身属于父组件作用域)
插槽内容本身属于父组件的作用域,因此可以直接使用父组件的数据,这相当于间接向子组件的插槽区域传递了父组件数据。
父组件:定义数据并在插槽中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25<script setup>
import { ref } from 'vue'
import WordContainer from './WordContainer.vue'
// 父组件的响应式数据
const userInfo = ref({
name: '张三',
level: '高级学习者'
})
const favoriteColor = ref('blue')
</script>
<template>
<WordContainer>
<!-- 插槽内容中直接使用父组件的数据 -->
<div class="user-info">
<p>用户名:{{ userInfo.name }}</p>
<p>等级:{{ userInfo.level }}</p>
<div :style="{ color: favoriteColor }">
自定义颜色文本
</div>
</div>
</WordContainer>
</template>子组件:渲染插槽内容(自动包含父组件数据)
1
2
3
4
5
6<template>
<div class="word-container">
<!-- 插槽会渲染父组件传递的内容,包含父组件数据 -->
<slot />
</div>
</template>
事件与插槽的结合
通过插槽(#actions)向子组件 WordCard
传递按钮,按钮的点击事件最终由 WordList
处理并触发向上传递的事件:
1 | <WordCard ...> |
- 插槽中的按钮属于
WordList组件的上下文,因此点击事件直接绑定到WordList的方法(handleEdit等) - 这些方法再通过
emit向WordList的父组件传递事件,形成 “多层传递”






